tiny-http-mcp-server 0.1.33 → 0.1.35
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/composition.json +1 -1
- package/node_modules/mcp-oauth/README.md +25 -2
- package/node_modules/mcp-oauth/dist/client/client-registration.js +9 -0
- package/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +91 -9
- package/node_modules/mcp-oauth/dist/client/token-auth-method.d.ts +2 -0
- package/node_modules/mcp-oauth/dist/client/token-auth-method.js +7 -0
- package/node_modules/mcp-oauth/dist/client/token-endpoint.d.ts +3 -1
- package/node_modules/mcp-oauth/dist/client/token-endpoint.js +19 -9
- package/node_modules/mcp-oauth/dist/client/types.d.ts +13 -1
- package/node_modules/mcp-oauth/dist/index.d.ts +1 -1
- package/node_modules/tiny-mcp-client/dist/index.d.ts +13 -1
- package/node_modules/tiny-mcp-client/dist/index.js +119 -19
- package/package.json +1 -1
package/dist/composition.json
CHANGED
|
@@ -106,8 +106,13 @@ Select a separate persistence namespace for another scope profile. No scope is
|
|
|
106
106
|
invented when the client does not configure one.
|
|
107
107
|
|
|
108
108
|
Imported `initialGrant.tokens` use `accessToken`, optional `refreshToken`,
|
|
109
|
-
`tokenType: "Bearer"`, `expiresAt` (Unix epoch milliseconds or `null`
|
|
110
|
-
and optional `scope`.
|
|
109
|
+
`tokenType: "Bearer"`, optional `expiresAt` (Unix epoch milliseconds or `null`
|
|
110
|
+
if unknown), and optional `scope`. `expiresIn` is a lifetime in seconds and is
|
|
111
|
+
anchored once at import. For a delayed import, provide the original `issuedAt`
|
|
112
|
+
in epoch milliseconds or its real absolute `expiresAt`; a numeric absolute
|
|
113
|
+
expiry takes precedence. Without either, the relative value means remaining
|
|
114
|
+
lifetime at import. Omitted expiry stays unknown. A fresh imported token is
|
|
115
|
+
used only for its resource.
|
|
111
116
|
Discovery binds an expired or explicitly rejected grant before silent refresh,
|
|
112
117
|
using the original configured client. Persisted sessions take precedence,
|
|
113
118
|
including sessions whose tokens have been cleared; an import cannot revive them.
|
|
@@ -119,6 +124,24 @@ Explicit ID/secret values must agree with the imported response. Sessions and
|
|
|
119
124
|
native registration stores retain arrays, issuance/expiry timestamps and JSON
|
|
120
125
|
provider metadata. Registration input is copied, bounded to 64 KiB and 64
|
|
121
126
|
levels, and rejects invalid standard field types and non-JSON extensions.
|
|
127
|
+
An optional registration `issuer` must match discovery and the persisted
|
|
128
|
+
authorization server exactly. Contradictory metadata fails without activating
|
|
129
|
+
or redeeming the grant. `client_secret_expires_at` uses Unix epoch seconds;
|
|
130
|
+
zero means no expiry. Live access tokens remain usable after secret expiry,
|
|
131
|
+
but an expired secret is never submitted for refresh. Native DCR can replace
|
|
132
|
+
an expired registration during explicit authorization; caller-owned imports
|
|
133
|
+
must be updated. Headless requests retain the old record and report recovery
|
|
134
|
+
is required without creating a pending refresh marker.
|
|
135
|
+
Set `client.tokenEndpointAuthMethod` to `none`, `client_secret_post` or
|
|
136
|
+
`client_secret_basic`; a full registration can supply the same field as
|
|
137
|
+
`token_endpoint_auth_method`. Public clients never transmit a stored secret.
|
|
138
|
+
Basic credentials are individually form-encoded before Base64 encoding and
|
|
139
|
+
are omitted from the form body. Cached grants retain their registered method;
|
|
140
|
+
an explicitly different configured method requires separate persistence or a
|
|
141
|
+
reset. Native DCR chooses a supported method, preferring public PKCE when
|
|
142
|
+
advertised. Unsupported methods and missing confidential secrets fail before
|
|
143
|
+
token requests. Existing clients without a method keep the previous default:
|
|
144
|
+
body authentication when a secret is present, public authentication otherwise.
|
|
122
145
|
Static clients and dynamic initial-grant imports require cached grants to match
|
|
123
146
|
the original normalized client ID and secret. A different client configuration
|
|
124
147
|
fails before attaching or refreshing credentials and retains the stored record;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { normalizeOAuthScope } from "./scope.js";
|
|
2
|
+
import { normalizeOAuthTokenEndpointAuthMethod } from "./token-auth-method.js";
|
|
2
3
|
/** Validate and copy a bounded JSON DCR response without quoting credential input. */
|
|
3
4
|
export function parseOAuthClientRegistration(value) {
|
|
4
5
|
const invalid = () => new Error("Invalid OAuth client registration metadata");
|
|
@@ -83,12 +84,20 @@ export function normalizeStoredOAuthClient(value) {
|
|
|
83
84
|
(clientSecret !== undefined && (typeof clientSecret !== "string" || clientSecret.trim() === "")))
|
|
84
85
|
return null;
|
|
85
86
|
const client = { clientId: clientId.trim(), ...(clientSecret === undefined ? {} : { clientSecret: clientSecret.trim() }) };
|
|
87
|
+
const method = normalizeOAuthTokenEndpointAuthMethod(Object.hasOwn(record, "tokenEndpointAuthMethod") ? record.tokenEndpointAuthMethod : undefined);
|
|
86
88
|
if (Object.hasOwn(record, "registration") && record.registration !== undefined) {
|
|
87
89
|
const registration = parseOAuthClientRegistration(record.registration);
|
|
88
90
|
const registeredSecret = Object.hasOwn(registration, "client_secret") ? registration.client_secret?.trim() : undefined;
|
|
89
91
|
if (registration.client_id.trim() !== client.clientId || registeredSecret !== client.clientSecret)
|
|
90
92
|
throw new Error("OAuth client registration does not match the client identity");
|
|
91
93
|
client.registration = registration;
|
|
94
|
+
const registrationMethod = normalizeOAuthTokenEndpointAuthMethod(Object.hasOwn(registration, "token_endpoint_auth_method") ? registration.token_endpoint_auth_method : undefined);
|
|
95
|
+
if (method !== undefined && registrationMethod !== undefined && method !== registrationMethod)
|
|
96
|
+
throw new Error("OAuth token endpoint authentication conflicts with the client registration");
|
|
97
|
+
if (registrationMethod !== undefined)
|
|
98
|
+
client.tokenEndpointAuthMethod = registrationMethod;
|
|
92
99
|
}
|
|
100
|
+
if (method !== undefined)
|
|
101
|
+
client.tokenEndpointAuthMethod = method;
|
|
93
102
|
return client;
|
|
94
103
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { normalizeStoredOAuthClient, parseOAuthClientRegistration } from "./client-registration.js";
|
|
2
2
|
import { normalizeOAuthScope } from "./scope.js";
|
|
3
|
+
import { normalizeOAuthTokenEndpointAuthMethod } from "./token-auth-method.js";
|
|
3
4
|
import { isIP } from "node:net";
|
|
4
5
|
import { fetchMcpResponse } from "../http-fetch.js";
|
|
5
6
|
import { URL } from "node:url";
|
|
@@ -23,6 +24,8 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
23
24
|
const clientMetadata = getClientMetadata(options.client);
|
|
24
25
|
const requestedScope = clientMetadata?.scope;
|
|
25
26
|
const configuredClient = normalizeConfiguredClient(options.client);
|
|
27
|
+
const requestedTokenMethod = normalizeOAuthTokenEndpointAuthMethod(options.client.tokenEndpointAuthMethod);
|
|
28
|
+
const configuredTokenMethod = requestedTokenMethod ?? configuredClient?.tokenEndpointAuthMethod;
|
|
26
29
|
const sessionStore = options.sessionStore ?? createAuthStoreSessionStore(options.authStore, options.persistenceNamespace);
|
|
27
30
|
const clientStore = options.authStore === undefined ? null : createAuthStoreClientStore(options.authStore, options.persistenceNamespace);
|
|
28
31
|
const now = options.now ?? Date.now;
|
|
@@ -40,7 +43,7 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
40
43
|
}
|
|
41
44
|
const initialGrant = options.initialGrant === undefined ? undefined : {
|
|
42
45
|
resource: canonicalizeResourceIndicator(options.initialGrant.resource),
|
|
43
|
-
tokens:
|
|
46
|
+
tokens: normalizeImportedTokens(options.initialGrant.tokens, now),
|
|
44
47
|
client: configuredClient
|
|
45
48
|
};
|
|
46
49
|
if (initialGrant !== undefined && (initialGrant.tokens === undefined || initialGrant.client === null))
|
|
@@ -124,6 +127,10 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
124
127
|
const canonicalResource = canonicalizeResourceIndicator(resource);
|
|
125
128
|
return withOAuthSessionTransaction(sessionStore, canonicalResource, async () => {
|
|
126
129
|
let session = await loadSession(canonicalResource);
|
|
130
|
+
if (session !== null)
|
|
131
|
+
assertRegistrationIssuer(session.client, session.authorizationServer);
|
|
132
|
+
if (configuredClient !== null && discovery !== undefined)
|
|
133
|
+
assertRegistrationIssuer(configuredClient, discovery.authorizationServer);
|
|
127
134
|
if (session !== null && initialGrant?.resource === canonicalResource)
|
|
128
135
|
initialGrantConsumed = true;
|
|
129
136
|
signal?.throwIfAborted();
|
|
@@ -156,6 +163,9 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
156
163
|
}
|
|
157
164
|
if (requestedScope !== undefined && session?.tokens !== undefined && normalizeOAuthScope(session.tokens.scope ?? session.requestedScope) !== requestedScope)
|
|
158
165
|
throw new Error("Stored session does not match the requested OAuth scope; authorize again or select separate persistence");
|
|
166
|
+
if (configuredTokenMethod !== undefined && session !== null && (session.tokens !== undefined || session.refreshState === "pending") &&
|
|
167
|
+
(session.client.tokenEndpointAuthMethod ?? (session.client.clientSecret === undefined ? "none" : "client_secret_post")) !== configuredTokenMethod)
|
|
168
|
+
throw new Error("Stored session does not match the requested OAuth token endpoint authentication; select separate persistence or reset it");
|
|
159
169
|
if (session?.refreshState === "pending") {
|
|
160
170
|
if (!allowInteractive || options.allowInteractive === false || sessionDiscovery === undefined)
|
|
161
171
|
throw new Error("OAuth refresh outcome is unknown; authorize again before using this resource");
|
|
@@ -167,6 +177,11 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
167
177
|
if (session?.tokens?.refreshToken !== undefined &&
|
|
168
178
|
sessionDiscovery !== undefined &&
|
|
169
179
|
(forceRefresh || isExpired(session.tokens, now))) {
|
|
180
|
+
if (hasExpiredClientSecret(session.client, now)) {
|
|
181
|
+
if (!allowInteractive || options.allowInteractive === false || options.client.mode === "static" || configuredClient?.registration !== undefined)
|
|
182
|
+
throw new Error("OAuth client secret has expired; authorize again or update the imported registration");
|
|
183
|
+
return authorizeSession(canonicalResource, clearSessionTokens(session), sessionDiscovery, fetch, signal);
|
|
184
|
+
}
|
|
170
185
|
session = await refreshSession(canonicalResource, session, sessionDiscovery, fetch, signal);
|
|
171
186
|
if (session?.tokens !== undefined && !isExpired(session.tokens, now)) {
|
|
172
187
|
return session;
|
|
@@ -190,6 +205,7 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
190
205
|
if (session.tokens?.refreshToken === undefined) {
|
|
191
206
|
return session;
|
|
192
207
|
}
|
|
208
|
+
assertTokenEndpointAuthentication(session.client, discovery.authorizationServerMetadata);
|
|
193
209
|
const pendingSession = { ...clearSessionTokens(session), refreshState: "pending" };
|
|
194
210
|
await saveSession(resource, pendingSession);
|
|
195
211
|
signal?.throwIfAborted();
|
|
@@ -201,6 +217,7 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
201
217
|
tokenEndpoint: requireOwnString(discovery.authorizationServerMetadata, "token_endpoint", "Authorization server metadata"),
|
|
202
218
|
clientId: session.client.clientId,
|
|
203
219
|
clientSecret: session.client.clientSecret,
|
|
220
|
+
tokenEndpointAuthMethod: session.client.tokenEndpointAuthMethod,
|
|
204
221
|
refreshToken: session.tokens.refreshToken,
|
|
205
222
|
resource,
|
|
206
223
|
fetch, signal,
|
|
@@ -266,6 +283,7 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
266
283
|
let resolvedClient = null;
|
|
267
284
|
try {
|
|
268
285
|
resolvedClient = await resolveClient(currentSession, discovery, loopback.redirectUri, fetch, signal);
|
|
286
|
+
assertTokenEndpointAuthentication(resolvedClient.client, discovery.authorizationServerMetadata);
|
|
269
287
|
const sessionWithoutTokens = {
|
|
270
288
|
resource,
|
|
271
289
|
authorizationServer: discovery.authorizationServer,
|
|
@@ -289,6 +307,7 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
289
307
|
tokenEndpoint: requireOwnString(discovery.authorizationServerMetadata, "token_endpoint", "Authorization server metadata"),
|
|
290
308
|
clientId: resolvedClient.client.clientId,
|
|
291
309
|
clientSecret: resolvedClient.client.clientSecret,
|
|
310
|
+
tokenEndpointAuthMethod: resolvedClient.client.tokenEndpointAuthMethod,
|
|
292
311
|
code,
|
|
293
312
|
codeVerifier: verifier,
|
|
294
313
|
redirectUri: loopback.redirectUri,
|
|
@@ -333,6 +352,9 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
333
352
|
if (configuredClient === null) {
|
|
334
353
|
throw new Error("OAuth client_id must not be blank");
|
|
335
354
|
}
|
|
355
|
+
assertRegistrationIssuer(configuredClient, discovery.authorizationServer);
|
|
356
|
+
if (hasExpiredClientSecret(configuredClient, now))
|
|
357
|
+
throw new Error("OAuth client secret has expired; update the imported registration");
|
|
336
358
|
return {
|
|
337
359
|
kind: "static",
|
|
338
360
|
fromStoredRegistration: false,
|
|
@@ -347,7 +369,14 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
347
369
|
client: configuredClient
|
|
348
370
|
};
|
|
349
371
|
}
|
|
350
|
-
|
|
372
|
+
let storedClient = await loadRegisteredClient(discovery.authorizationServer);
|
|
373
|
+
if (storedClient !== null) {
|
|
374
|
+
assertRegistrationIssuer(storedClient, discovery.authorizationServer);
|
|
375
|
+
if (hasExpiredClientSecret(storedClient, now)) {
|
|
376
|
+
await clearRegisteredClient(discovery.authorizationServer);
|
|
377
|
+
storedClient = null;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
351
380
|
if (storedClient !== null) {
|
|
352
381
|
return {
|
|
353
382
|
kind: "dynamic",
|
|
@@ -356,7 +385,7 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
356
385
|
};
|
|
357
386
|
}
|
|
358
387
|
if (registrationEndpoint === undefined) {
|
|
359
|
-
if (existingSession !== null && existingSession.client.clientId.length > 0) {
|
|
388
|
+
if (existingSession !== null && existingSession.client.clientId.length > 0 && !hasExpiredClientSecret(existingSession.client, now)) {
|
|
360
389
|
return {
|
|
361
390
|
kind: "dynamic",
|
|
362
391
|
fromStoredRegistration: true,
|
|
@@ -365,7 +394,7 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
365
394
|
}
|
|
366
395
|
throw new Error("Authorization server metadata is missing registration_endpoint");
|
|
367
396
|
}
|
|
368
|
-
if (existingSession !== null && existingSession.client.clientId.length > 0) {
|
|
397
|
+
if (existingSession !== null && existingSession.client.clientId.length > 0 && !hasExpiredClientSecret(existingSession.client, now)) {
|
|
369
398
|
const isConfiguredStaticFallback = configuredClient !== null &&
|
|
370
399
|
existingSession.client.clientId === configuredClient.clientId &&
|
|
371
400
|
existingSession.client.clientSecret === configuredClient.clientSecret;
|
|
@@ -378,7 +407,12 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
378
407
|
};
|
|
379
408
|
}
|
|
380
409
|
}
|
|
381
|
-
const
|
|
410
|
+
const supported = getSupportedTokenAuthMethods(discovery.authorizationServerMetadata);
|
|
411
|
+
const registrationMethod = requestedTokenMethod ?? (supported === undefined ? "none" :
|
|
412
|
+
["none", "client_secret_basic", "client_secret_post"].find(method => supported.includes(method)));
|
|
413
|
+
if (registrationMethod === undefined || (supported !== undefined && !supported.includes(registrationMethod)))
|
|
414
|
+
throw new Error("Authorization server does not support the requested OAuth token endpoint authentication");
|
|
415
|
+
const registrationBody = buildClientRegistrationBody(clientMetadata, redirectUri, registrationMethod);
|
|
382
416
|
const deadline = AbortSignal.timeout(30_000);
|
|
383
417
|
const signal = parentSignal === undefined ? deadline : AbortSignal.any([parentSignal, deadline]);
|
|
384
418
|
const response = await fetchMcpResponse(fetch, registrationEndpoint, {
|
|
@@ -392,11 +426,17 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
392
426
|
const payload = await readOAuthJsonObjectResponse(response, signal);
|
|
393
427
|
const registration = parseOAuthClientRegistration(payload);
|
|
394
428
|
const registeredSecret = getOwnString(registration, "client_secret");
|
|
429
|
+
const responseMethod = normalizeOAuthTokenEndpointAuthMethod(getOwnEntry(registration, "token_endpoint_auth_method")) ??
|
|
430
|
+
requestedTokenMethod ?? (supported === undefined ? undefined : normalizeOAuthTokenEndpointAuthMethod(registrationMethod));
|
|
395
431
|
const registeredClient = {
|
|
396
432
|
clientId: registration.client_id.trim(),
|
|
397
433
|
...(registeredSecret === undefined ? {} : { clientSecret: registeredSecret.trim() }),
|
|
434
|
+
...(responseMethod === undefined ? {} : { tokenEndpointAuthMethod: responseMethod }),
|
|
398
435
|
registration
|
|
399
436
|
};
|
|
437
|
+
assertRegistrationIssuer(registeredClient, discovery.authorizationServer);
|
|
438
|
+
if (hasExpiredClientSecret(registeredClient, now))
|
|
439
|
+
throw new Error("OAuth client secret has expired in the registration response");
|
|
400
440
|
await saveRegisteredClient(discovery.authorizationServer, registeredClient);
|
|
401
441
|
return {
|
|
402
442
|
kind: "dynamic",
|
|
@@ -408,7 +448,7 @@ export function createDefaultOAuthClientProvider(options) {
|
|
|
408
448
|
return normalizeLoadedSession(await sessionStore.load(resource));
|
|
409
449
|
}
|
|
410
450
|
async function saveSession(resource, session) {
|
|
411
|
-
await sessionStore.save(resource, session);
|
|
451
|
+
await sessionStore.save(resource, structuredClone(session));
|
|
412
452
|
}
|
|
413
453
|
async function clearSession(resource) {
|
|
414
454
|
await sessionStore.clear(resource);
|
|
@@ -510,6 +550,21 @@ function normalizeLoadedSession(session) {
|
|
|
510
550
|
tokens: normalizeStoredTokens(getOwnEntry(session, "tokens"))
|
|
511
551
|
};
|
|
512
552
|
}
|
|
553
|
+
function normalizeImportedTokens(value, now) {
|
|
554
|
+
if (!isObjectRecord(value))
|
|
555
|
+
return undefined;
|
|
556
|
+
const absolute = getOwnEntry(value, "expiresAt");
|
|
557
|
+
const lifetime = getOwnEntry(value, "expiresIn");
|
|
558
|
+
const issuedAt = getOwnEntry(value, "issuedAt");
|
|
559
|
+
if (lifetime !== undefined && (typeof lifetime !== "number" || !Number.isSafeInteger(lifetime) || lifetime < 0))
|
|
560
|
+
throw new Error("OAuth initial grant has invalid relative expiry");
|
|
561
|
+
if (issuedAt !== undefined && (typeof issuedAt !== "number" || !Number.isSafeInteger(issuedAt) ||
|
|
562
|
+
Math.abs(issuedAt) > MAX_JS_DATE_MS))
|
|
563
|
+
throw new Error("OAuth initial grant has invalid issuance time");
|
|
564
|
+
const expiresAt = absolute !== undefined && absolute !== null ? absolute :
|
|
565
|
+
lifetime === undefined ? null : (issuedAt === undefined ? now() : issuedAt) + lifetime * 1000;
|
|
566
|
+
return normalizeStoredTokens({ ...value, expiresAt });
|
|
567
|
+
}
|
|
513
568
|
function normalizeStoredTokens(value) {
|
|
514
569
|
if (value === undefined || !isObjectRecord(value)) {
|
|
515
570
|
return undefined;
|
|
@@ -564,7 +619,7 @@ function normalizeConfiguredClient(client) {
|
|
|
564
619
|
if (clientId === undefined)
|
|
565
620
|
return null;
|
|
566
621
|
const clientSecret = normalizeOptionalOAuthString(client.clientSecret) ?? (registration === undefined ? undefined : getOwnString(registration, "client_secret")?.trim());
|
|
567
|
-
return normalizeStoredOAuthClient({ clientId, clientSecret, registration });
|
|
622
|
+
return normalizeStoredOAuthClient({ clientId, clientSecret, registration, tokenEndpointAuthMethod: client.tokenEndpointAuthMethod });
|
|
568
623
|
}
|
|
569
624
|
function normalizeOptionalOAuthString(value) {
|
|
570
625
|
if (value === undefined) {
|
|
@@ -671,12 +726,39 @@ function assertRequestMatchesResource(requestUrl, resource) {
|
|
|
671
726
|
throw new Error(`OAuth request URL ${requestUrl} does not match discovered resource ${resource}`);
|
|
672
727
|
}
|
|
673
728
|
}
|
|
674
|
-
function
|
|
729
|
+
function assertRegistrationIssuer(client, issuer) {
|
|
730
|
+
const registrationIssuer = client.registration === undefined ? undefined : getOwnString(client.registration, "issuer");
|
|
731
|
+
if (registrationIssuer !== undefined && registrationIssuer !== issuer)
|
|
732
|
+
throw new Error("OAuth client registration issuer does not match the authorization server");
|
|
733
|
+
}
|
|
734
|
+
function hasExpiredClientSecret(client, now) {
|
|
735
|
+
if (client.clientSecret === undefined || client.tokenEndpointAuthMethod === "none" || client.registration === undefined)
|
|
736
|
+
return false;
|
|
737
|
+
const expiry = getOwnEntry(client.registration, "client_secret_expires_at");
|
|
738
|
+
return typeof expiry === "number" && expiry !== 0 && expiry <= now() / 1000;
|
|
739
|
+
}
|
|
740
|
+
function getSupportedTokenAuthMethods(metadata) {
|
|
741
|
+
const value = getOwnEntry(metadata, "token_endpoint_auth_methods_supported");
|
|
742
|
+
if (value === undefined)
|
|
743
|
+
return undefined;
|
|
744
|
+
if (!Array.isArray(value) || value.length > 128 || value.some(method => typeof method !== "string"))
|
|
745
|
+
throw new Error("Invalid OAuth token endpoint authentication metadata");
|
|
746
|
+
return value;
|
|
747
|
+
}
|
|
748
|
+
function assertTokenEndpointAuthentication(client, metadata) {
|
|
749
|
+
const method = client.tokenEndpointAuthMethod ?? (client.clientSecret === undefined ? "none" : "client_secret_post");
|
|
750
|
+
if (method !== "none" && client.clientSecret === undefined)
|
|
751
|
+
throw new Error("OAuth token endpoint authentication requires a client secret");
|
|
752
|
+
const supported = getSupportedTokenAuthMethods(metadata);
|
|
753
|
+
if (supported !== undefined && !supported.includes(method))
|
|
754
|
+
throw new Error("Authorization server does not support the requested OAuth token endpoint authentication");
|
|
755
|
+
}
|
|
756
|
+
function buildClientRegistrationBody(metadata, redirectUri, tokenEndpointAuthMethod) {
|
|
675
757
|
const body = {
|
|
676
758
|
redirect_uris: [redirectUri],
|
|
677
759
|
grant_types: ["authorization_code", "refresh_token"],
|
|
678
760
|
response_types: ["code"],
|
|
679
|
-
token_endpoint_auth_method:
|
|
761
|
+
token_endpoint_auth_method: tokenEndpointAuthMethod
|
|
680
762
|
};
|
|
681
763
|
const clientName = metadata === undefined ? undefined : getOwnString(metadata, "clientName");
|
|
682
764
|
const scope = metadata === undefined ? undefined : getOwnString(metadata, "scope");
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export function normalizeOAuthTokenEndpointAuthMethod(value) {
|
|
2
|
+
if (value === undefined || value === null)
|
|
3
|
+
return undefined;
|
|
4
|
+
if (value !== "none" && value !== "client_secret_post" && value !== "client_secret_basic")
|
|
5
|
+
throw new Error("Unsupported OAuth token endpoint authentication method");
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { OAuthMetadataFetch, StoredOAuthTokens } from "./types.js";
|
|
1
|
+
import type { OAuthMetadataFetch, StoredOAuthTokens, OAuthTokenEndpointAuthMethod } from "./types.js";
|
|
2
2
|
interface OAuthErrorShape {
|
|
3
3
|
error: string;
|
|
4
4
|
error_description?: string;
|
|
@@ -22,6 +22,7 @@ export declare function exchangeAuthorizationCode(input: {
|
|
|
22
22
|
tokenEndpoint: string;
|
|
23
23
|
clientId: string;
|
|
24
24
|
clientSecret?: string;
|
|
25
|
+
tokenEndpointAuthMethod?: OAuthTokenEndpointAuthMethod;
|
|
25
26
|
code: string;
|
|
26
27
|
codeVerifier: string;
|
|
27
28
|
redirectUri: string;
|
|
@@ -34,6 +35,7 @@ export declare function refreshAccessToken(input: {
|
|
|
34
35
|
tokenEndpoint: string;
|
|
35
36
|
clientId: string;
|
|
36
37
|
clientSecret?: string;
|
|
38
|
+
tokenEndpointAuthMethod?: OAuthTokenEndpointAuthMethod;
|
|
37
39
|
refreshToken: string;
|
|
38
40
|
resource: string;
|
|
39
41
|
fetch: OAuthMetadataFetch;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeOAuthTokenEndpointAuthMethod } from "./token-auth-method.js";
|
|
1
2
|
import { canonicalizeResourceIndicator } from "../resource-indicator.js";
|
|
2
3
|
import { readBoundedResponseText } from "../http-response.js";
|
|
3
4
|
import { fetchMcpResponse } from "../http-fetch.js";
|
|
@@ -40,6 +41,7 @@ export async function exchangeAuthorizationCode(input) {
|
|
|
40
41
|
tokenEndpoint: input.tokenEndpoint,
|
|
41
42
|
clientId: input.clientId,
|
|
42
43
|
clientSecret: input.clientSecret,
|
|
44
|
+
tokenEndpointAuthMethod: input.tokenEndpointAuthMethod,
|
|
43
45
|
params: {
|
|
44
46
|
grant_type: "authorization_code",
|
|
45
47
|
code: input.code,
|
|
@@ -58,6 +60,7 @@ export async function refreshAccessToken(input) {
|
|
|
58
60
|
tokenEndpoint: input.tokenEndpoint,
|
|
59
61
|
clientId: input.clientId,
|
|
60
62
|
clientSecret: input.clientSecret,
|
|
63
|
+
tokenEndpointAuthMethod: input.tokenEndpointAuthMethod,
|
|
61
64
|
params: {
|
|
62
65
|
grant_type: "refresh_token",
|
|
63
66
|
refresh_token: input.refreshToken,
|
|
@@ -69,21 +72,28 @@ export async function refreshAccessToken(input) {
|
|
|
69
72
|
});
|
|
70
73
|
}
|
|
71
74
|
async function requestTokens(input) {
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
const method = normalizeOAuthTokenEndpointAuthMethod(input.tokenEndpointAuthMethod) ??
|
|
76
|
+
(input.clientSecret === undefined ? "none" : "client_secret_post");
|
|
77
|
+
if (method !== "none" && (input.clientSecret === undefined || input.clientSecret.trim() === ""))
|
|
78
|
+
throw new Error("OAuth token endpoint authentication requires a client secret");
|
|
79
|
+
const body = new URLSearchParams(input.params);
|
|
80
|
+
const headers = new Headers({ "Content-Type": "application/x-www-form-urlencoded" });
|
|
81
|
+
if (method === "client_secret_basic") {
|
|
82
|
+
const encoded = new URLSearchParams({ credential: input.clientId }).toString().slice("credential=".length);
|
|
83
|
+
const encodedSecret = new URLSearchParams({ credential: input.clientSecret }).toString().slice("credential=".length);
|
|
84
|
+
headers.set("Authorization", `Basic ${Buffer.from(`${encoded}:${encodedSecret}`).toString("base64")}`);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
body.set("client_id", input.clientId);
|
|
88
|
+
if (method === "client_secret_post")
|
|
89
|
+
body.set("client_secret", input.clientSecret);
|
|
78
90
|
}
|
|
79
91
|
input.signal?.throwIfAborted();
|
|
80
92
|
const deadline = AbortSignal.timeout(30_000);
|
|
81
93
|
const signal = input.signal === undefined ? deadline : AbortSignal.any([input.signal, deadline]);
|
|
82
94
|
const response = await fetchMcpResponse(input.fetch, input.tokenEndpoint, {
|
|
83
95
|
method: "POST",
|
|
84
|
-
headers
|
|
85
|
-
"Content-Type": "application/x-www-form-urlencoded"
|
|
86
|
-
},
|
|
96
|
+
headers,
|
|
87
97
|
body: body.toString(),
|
|
88
98
|
signal
|
|
89
99
|
});
|
|
@@ -70,6 +70,14 @@ export interface StoredOAuthTokens {
|
|
|
70
70
|
expiresAt: number | null;
|
|
71
71
|
scope?: string;
|
|
72
72
|
}
|
|
73
|
+
/** Import-time lifetimes are normalized once into persisted epoch milliseconds. */
|
|
74
|
+
export interface ImportedOAuthTokens extends Omit<StoredOAuthTokens, "expiresAt"> {
|
|
75
|
+
expiresAt?: number | null;
|
|
76
|
+
/** Remaining lifetime at import, or total lifetime when issuedAt is provided. */
|
|
77
|
+
expiresIn?: number;
|
|
78
|
+
/** Original issuance time in Unix epoch milliseconds. */
|
|
79
|
+
issuedAt?: number;
|
|
80
|
+
}
|
|
73
81
|
/** Full RFC 7591 response, including JSON provider extensions. */
|
|
74
82
|
export interface OAuthClientRegistration extends Record<string, unknown> {
|
|
75
83
|
client_id: string;
|
|
@@ -86,7 +94,9 @@ export interface StoredOAuthClient {
|
|
|
86
94
|
clientId: string;
|
|
87
95
|
clientSecret?: string;
|
|
88
96
|
registration?: OAuthClientRegistration;
|
|
97
|
+
tokenEndpointAuthMethod?: OAuthTokenEndpointAuthMethod;
|
|
89
98
|
}
|
|
99
|
+
export type OAuthTokenEndpointAuthMethod = "none" | "client_secret_post" | "client_secret_basic";
|
|
90
100
|
export interface StoredOAuthSession {
|
|
91
101
|
resource: string;
|
|
92
102
|
authorizationServer: string;
|
|
@@ -120,12 +130,14 @@ export interface DefaultOAuthClientProviderOptions {
|
|
|
120
130
|
metadata?: OAuthClientMetadata;
|
|
121
131
|
/** Import a complete registration owned by the caller. */
|
|
122
132
|
registration?: OAuthClientRegistration;
|
|
133
|
+
tokenEndpointAuthMethod?: OAuthTokenEndpointAuthMethod;
|
|
123
134
|
} | {
|
|
124
135
|
mode: "static";
|
|
125
136
|
clientId: string;
|
|
126
137
|
clientSecret?: string;
|
|
127
138
|
metadata?: OAuthClientMetadata;
|
|
128
139
|
registration?: OAuthClientRegistration;
|
|
140
|
+
tokenEndpointAuthMethod?: OAuthTokenEndpointAuthMethod;
|
|
129
141
|
};
|
|
130
142
|
/** Disable interactive authorization while allowing cached tokens and silent refresh. */
|
|
131
143
|
allowInteractive?: boolean;
|
|
@@ -136,7 +148,7 @@ export interface DefaultOAuthClientProviderOptions {
|
|
|
136
148
|
/** Import an existing grant for one resource. Persisted sessions take precedence. */
|
|
137
149
|
initialGrant?: {
|
|
138
150
|
resource: string;
|
|
139
|
-
tokens:
|
|
151
|
+
tokens: ImportedOAuthTokens;
|
|
140
152
|
};
|
|
141
153
|
browser: {
|
|
142
154
|
openBrowser?(url: string): Promise<void>;
|
|
@@ -6,7 +6,7 @@ export { generateCodeChallenge, generateCodeVerifier, } from "./client/pkce.js";
|
|
|
6
6
|
export { OAuthError, } from "./client/token-endpoint.js";
|
|
7
7
|
export { canonicalizeResourceIndicator, } from "./resource-indicator.js";
|
|
8
8
|
export { createJwksTokenVerifier, } from "./server/jwks-token-verifier.js";
|
|
9
|
-
export type { DefaultOAuthClientProviderOptions, OAuthAuthorizationServerMetadata, OAuthClientMetadata, OAuthClientRegistration, OAuthClientProvider, OAuthClientProviderOptions, OAuthDiscoveryResult, OAuthMetadataFetch, OAuthProtectedResourceMetadata, OAuthSessionStore, OAuthUnauthorizedChallenge, StoredOAuthSession, StoredOAuthClient, StoredOAuthTokens, } from "./client/types.js";
|
|
9
|
+
export type { DefaultOAuthClientProviderOptions, OAuthAuthorizationServerMetadata, OAuthClientMetadata, OAuthClientRegistration, OAuthClientProvider, OAuthClientProviderOptions, OAuthDiscoveryResult, OAuthMetadataFetch, OAuthProtectedResourceMetadata, OAuthSessionStore, OAuthUnauthorizedChallenge, OAuthTokenEndpointAuthMethod, StoredOAuthSession, StoredOAuthClient, StoredOAuthTokens, ImportedOAuthTokens, } from "./client/types.js";
|
|
10
10
|
export type { JwksTokenVerifier, JwksTokenVerifierOptions, JwksVerifiedAccessToken, } from "./server/jwks-token-verifier.js";
|
|
11
11
|
export type { LoopbackAuthorizationOptions, LoopbackAuthorizationSession, OAuthLandingPage, } from "./client/loopback-authorization.js";
|
|
12
12
|
export { readBoundedResponseText } from "./http-response.js";
|
|
@@ -173,6 +173,14 @@ interface StoredOAuthTokens {
|
|
|
173
173
|
expiresAt: number | null;
|
|
174
174
|
scope?: string;
|
|
175
175
|
}
|
|
176
|
+
/** Import-time lifetimes are normalized once into persisted epoch milliseconds. */
|
|
177
|
+
interface ImportedOAuthTokens extends Omit<StoredOAuthTokens, "expiresAt"> {
|
|
178
|
+
expiresAt?: number | null;
|
|
179
|
+
/** Remaining lifetime at import, or total lifetime when issuedAt is provided. */
|
|
180
|
+
expiresIn?: number;
|
|
181
|
+
/** Original issuance time in Unix epoch milliseconds. */
|
|
182
|
+
issuedAt?: number;
|
|
183
|
+
}
|
|
176
184
|
/** Full RFC 7591 response, including JSON provider extensions. */
|
|
177
185
|
interface OAuthClientRegistration extends Record<string, unknown> {
|
|
178
186
|
client_id: string;
|
|
@@ -189,7 +197,9 @@ interface StoredOAuthClient {
|
|
|
189
197
|
clientId: string;
|
|
190
198
|
clientSecret?: string;
|
|
191
199
|
registration?: OAuthClientRegistration;
|
|
200
|
+
tokenEndpointAuthMethod?: OAuthTokenEndpointAuthMethod;
|
|
192
201
|
}
|
|
202
|
+
type OAuthTokenEndpointAuthMethod = "none" | "client_secret_post" | "client_secret_basic";
|
|
193
203
|
interface StoredOAuthSession {
|
|
194
204
|
resource: string;
|
|
195
205
|
authorizationServer: string;
|
|
@@ -223,12 +233,14 @@ interface DefaultOAuthClientProviderOptions {
|
|
|
223
233
|
metadata?: OAuthClientMetadata;
|
|
224
234
|
/** Import a complete registration owned by the caller. */
|
|
225
235
|
registration?: OAuthClientRegistration;
|
|
236
|
+
tokenEndpointAuthMethod?: OAuthTokenEndpointAuthMethod;
|
|
226
237
|
} | {
|
|
227
238
|
mode: "static";
|
|
228
239
|
clientId: string;
|
|
229
240
|
clientSecret?: string;
|
|
230
241
|
metadata?: OAuthClientMetadata;
|
|
231
242
|
registration?: OAuthClientRegistration;
|
|
243
|
+
tokenEndpointAuthMethod?: OAuthTokenEndpointAuthMethod;
|
|
232
244
|
};
|
|
233
245
|
/** Disable interactive authorization while allowing cached tokens and silent refresh. */
|
|
234
246
|
allowInteractive?: boolean;
|
|
@@ -239,7 +251,7 @@ interface DefaultOAuthClientProviderOptions {
|
|
|
239
251
|
/** Import an existing grant for one resource. Persisted sessions take precedence. */
|
|
240
252
|
initialGrant?: {
|
|
241
253
|
resource: string;
|
|
242
|
-
tokens:
|
|
254
|
+
tokens: ImportedOAuthTokens;
|
|
243
255
|
};
|
|
244
256
|
browser: {
|
|
245
257
|
openBrowser?(url: string): Promise<void>;
|
|
@@ -3405,6 +3405,15 @@ function normalizeOAuthScope(scope) {
|
|
|
3405
3405
|
return normalized || void 0;
|
|
3406
3406
|
}
|
|
3407
3407
|
|
|
3408
|
+
// ../mcp-oauth/dist/client/token-auth-method.js
|
|
3409
|
+
function normalizeOAuthTokenEndpointAuthMethod(value) {
|
|
3410
|
+
if (value === void 0 || value === null)
|
|
3411
|
+
return void 0;
|
|
3412
|
+
if (value !== "none" && value !== "client_secret_post" && value !== "client_secret_basic")
|
|
3413
|
+
throw new Error("Unsupported OAuth token endpoint authentication method");
|
|
3414
|
+
return value;
|
|
3415
|
+
}
|
|
3416
|
+
|
|
3408
3417
|
// ../mcp-oauth/dist/client/client-registration.js
|
|
3409
3418
|
function parseOAuthClientRegistration(value) {
|
|
3410
3419
|
const invalid = () => new Error("Invalid OAuth client registration metadata");
|
|
@@ -3503,13 +3512,21 @@ function normalizeStoredOAuthClient(value) {
|
|
|
3503
3512
|
if (typeof clientId !== "string" || clientId.trim() === "" || clientSecret !== void 0 && (typeof clientSecret !== "string" || clientSecret.trim() === ""))
|
|
3504
3513
|
return null;
|
|
3505
3514
|
const client = { clientId: clientId.trim(), ...clientSecret === void 0 ? {} : { clientSecret: clientSecret.trim() } };
|
|
3515
|
+
const method = normalizeOAuthTokenEndpointAuthMethod(Object.hasOwn(record2, "tokenEndpointAuthMethod") ? record2.tokenEndpointAuthMethod : void 0);
|
|
3506
3516
|
if (Object.hasOwn(record2, "registration") && record2.registration !== void 0) {
|
|
3507
3517
|
const registration = parseOAuthClientRegistration(record2.registration);
|
|
3508
3518
|
const registeredSecret = Object.hasOwn(registration, "client_secret") ? registration.client_secret?.trim() : void 0;
|
|
3509
3519
|
if (registration.client_id.trim() !== client.clientId || registeredSecret !== client.clientSecret)
|
|
3510
3520
|
throw new Error("OAuth client registration does not match the client identity");
|
|
3511
3521
|
client.registration = registration;
|
|
3512
|
-
|
|
3522
|
+
const registrationMethod = normalizeOAuthTokenEndpointAuthMethod(Object.hasOwn(registration, "token_endpoint_auth_method") ? registration.token_endpoint_auth_method : void 0);
|
|
3523
|
+
if (method !== void 0 && registrationMethod !== void 0 && method !== registrationMethod)
|
|
3524
|
+
throw new Error("OAuth token endpoint authentication conflicts with the client registration");
|
|
3525
|
+
if (registrationMethod !== void 0)
|
|
3526
|
+
client.tokenEndpointAuthMethod = registrationMethod;
|
|
3527
|
+
}
|
|
3528
|
+
if (method !== void 0)
|
|
3529
|
+
client.tokenEndpointAuthMethod = method;
|
|
3513
3530
|
return client;
|
|
3514
3531
|
}
|
|
3515
3532
|
|
|
@@ -4760,6 +4777,7 @@ async function exchangeAuthorizationCode(input) {
|
|
|
4760
4777
|
tokenEndpoint: input.tokenEndpoint,
|
|
4761
4778
|
clientId: input.clientId,
|
|
4762
4779
|
clientSecret: input.clientSecret,
|
|
4780
|
+
tokenEndpointAuthMethod: input.tokenEndpointAuthMethod,
|
|
4763
4781
|
params: {
|
|
4764
4782
|
grant_type: "authorization_code",
|
|
4765
4783
|
code: input.code,
|
|
@@ -4778,6 +4796,7 @@ async function refreshAccessToken(input) {
|
|
|
4778
4796
|
tokenEndpoint: input.tokenEndpoint,
|
|
4779
4797
|
clientId: input.clientId,
|
|
4780
4798
|
clientSecret: input.clientSecret,
|
|
4799
|
+
tokenEndpointAuthMethod: input.tokenEndpointAuthMethod,
|
|
4781
4800
|
params: {
|
|
4782
4801
|
grant_type: "refresh_token",
|
|
4783
4802
|
refresh_token: input.refreshToken,
|
|
@@ -4789,21 +4808,26 @@ async function refreshAccessToken(input) {
|
|
|
4789
4808
|
});
|
|
4790
4809
|
}
|
|
4791
4810
|
async function requestTokens(input) {
|
|
4792
|
-
const
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4811
|
+
const method = normalizeOAuthTokenEndpointAuthMethod(input.tokenEndpointAuthMethod) ?? (input.clientSecret === void 0 ? "none" : "client_secret_post");
|
|
4812
|
+
if (method !== "none" && (input.clientSecret === void 0 || input.clientSecret.trim() === ""))
|
|
4813
|
+
throw new Error("OAuth token endpoint authentication requires a client secret");
|
|
4814
|
+
const body = new URLSearchParams(input.params);
|
|
4815
|
+
const headers = new Headers({ "Content-Type": "application/x-www-form-urlencoded" });
|
|
4816
|
+
if (method === "client_secret_basic") {
|
|
4817
|
+
const encoded = new URLSearchParams({ credential: input.clientId }).toString().slice("credential=".length);
|
|
4818
|
+
const encodedSecret = new URLSearchParams({ credential: input.clientSecret }).toString().slice("credential=".length);
|
|
4819
|
+
headers.set("Authorization", `Basic ${Buffer.from(`${encoded}:${encodedSecret}`).toString("base64")}`);
|
|
4820
|
+
} else {
|
|
4821
|
+
body.set("client_id", input.clientId);
|
|
4822
|
+
if (method === "client_secret_post")
|
|
4823
|
+
body.set("client_secret", input.clientSecret);
|
|
4798
4824
|
}
|
|
4799
4825
|
input.signal?.throwIfAborted();
|
|
4800
4826
|
const deadline = AbortSignal.timeout(3e4);
|
|
4801
4827
|
const signal = input.signal === void 0 ? deadline : AbortSignal.any([input.signal, deadline]);
|
|
4802
4828
|
const response = await fetchMcpResponse(input.fetch, input.tokenEndpoint, {
|
|
4803
4829
|
method: "POST",
|
|
4804
|
-
headers
|
|
4805
|
-
"Content-Type": "application/x-www-form-urlencoded"
|
|
4806
|
-
},
|
|
4830
|
+
headers,
|
|
4807
4831
|
body: body.toString(),
|
|
4808
4832
|
signal
|
|
4809
4833
|
});
|
|
@@ -4957,6 +4981,8 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
4957
4981
|
const clientMetadata = getClientMetadata(options.client);
|
|
4958
4982
|
const requestedScope = clientMetadata?.scope;
|
|
4959
4983
|
const configuredClient = normalizeConfiguredClient(options.client);
|
|
4984
|
+
const requestedTokenMethod = normalizeOAuthTokenEndpointAuthMethod(options.client.tokenEndpointAuthMethod);
|
|
4985
|
+
const configuredTokenMethod = requestedTokenMethod ?? configuredClient?.tokenEndpointAuthMethod;
|
|
4960
4986
|
const sessionStore = options.sessionStore ?? createAuthStoreSessionStore(options.authStore, options.persistenceNamespace);
|
|
4961
4987
|
const clientStore = options.authStore === void 0 ? null : createAuthStoreClientStore(options.authStore, options.persistenceNamespace);
|
|
4962
4988
|
const now = options.now ?? Date.now;
|
|
@@ -4973,7 +4999,7 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
4973
4999
|
}
|
|
4974
5000
|
const initialGrant = options.initialGrant === void 0 ? void 0 : {
|
|
4975
5001
|
resource: canonicalizeResourceIndicator(options.initialGrant.resource),
|
|
4976
|
-
tokens:
|
|
5002
|
+
tokens: normalizeImportedTokens(options.initialGrant.tokens, now),
|
|
4977
5003
|
client: configuredClient
|
|
4978
5004
|
};
|
|
4979
5005
|
if (initialGrant !== void 0 && (initialGrant.tokens === void 0 || initialGrant.client === null))
|
|
@@ -5051,6 +5077,10 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
5051
5077
|
const canonicalResource = canonicalizeResourceIndicator(resource);
|
|
5052
5078
|
return withOAuthSessionTransaction(sessionStore, canonicalResource, async () => {
|
|
5053
5079
|
let session = await loadSession(canonicalResource);
|
|
5080
|
+
if (session !== null)
|
|
5081
|
+
assertRegistrationIssuer(session.client, session.authorizationServer);
|
|
5082
|
+
if (configuredClient !== null && discovery !== void 0)
|
|
5083
|
+
assertRegistrationIssuer(configuredClient, discovery.authorizationServer);
|
|
5054
5084
|
if (session !== null && initialGrant?.resource === canonicalResource)
|
|
5055
5085
|
initialGrantConsumed = true;
|
|
5056
5086
|
signal?.throwIfAborted();
|
|
@@ -5085,6 +5115,8 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
5085
5115
|
}
|
|
5086
5116
|
if (requestedScope !== void 0 && session?.tokens !== void 0 && normalizeOAuthScope(session.tokens.scope ?? session.requestedScope) !== requestedScope)
|
|
5087
5117
|
throw new Error("Stored session does not match the requested OAuth scope; authorize again or select separate persistence");
|
|
5118
|
+
if (configuredTokenMethod !== void 0 && session !== null && (session.tokens !== void 0 || session.refreshState === "pending") && (session.client.tokenEndpointAuthMethod ?? (session.client.clientSecret === void 0 ? "none" : "client_secret_post")) !== configuredTokenMethod)
|
|
5119
|
+
throw new Error("Stored session does not match the requested OAuth token endpoint authentication; select separate persistence or reset it");
|
|
5088
5120
|
if (session?.refreshState === "pending") {
|
|
5089
5121
|
if (!allowInteractive || options.allowInteractive === false || sessionDiscovery === void 0)
|
|
5090
5122
|
throw new Error("OAuth refresh outcome is unknown; authorize again before using this resource");
|
|
@@ -5094,6 +5126,11 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
5094
5126
|
return session;
|
|
5095
5127
|
}
|
|
5096
5128
|
if (session?.tokens?.refreshToken !== void 0 && sessionDiscovery !== void 0 && (forceRefresh || isExpired(session.tokens, now))) {
|
|
5129
|
+
if (hasExpiredClientSecret(session.client, now)) {
|
|
5130
|
+
if (!allowInteractive || options.allowInteractive === false || options.client.mode === "static" || configuredClient?.registration !== void 0)
|
|
5131
|
+
throw new Error("OAuth client secret has expired; authorize again or update the imported registration");
|
|
5132
|
+
return authorizeSession(canonicalResource, clearSessionTokens(session), sessionDiscovery, fetch2, signal);
|
|
5133
|
+
}
|
|
5097
5134
|
session = await refreshSession(canonicalResource, session, sessionDiscovery, fetch2, signal);
|
|
5098
5135
|
if (session?.tokens !== void 0 && !isExpired(session.tokens, now)) {
|
|
5099
5136
|
return session;
|
|
@@ -5117,6 +5154,7 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
5117
5154
|
if (session.tokens?.refreshToken === void 0) {
|
|
5118
5155
|
return session;
|
|
5119
5156
|
}
|
|
5157
|
+
assertTokenEndpointAuthentication(session.client, discovery.authorizationServerMetadata);
|
|
5120
5158
|
const pendingSession = { ...clearSessionTokens(session), refreshState: "pending" };
|
|
5121
5159
|
await saveSession(resource, pendingSession);
|
|
5122
5160
|
signal?.throwIfAborted();
|
|
@@ -5128,6 +5166,7 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
5128
5166
|
tokenEndpoint: requireOwnString(discovery.authorizationServerMetadata, "token_endpoint", "Authorization server metadata"),
|
|
5129
5167
|
clientId: session.client.clientId,
|
|
5130
5168
|
clientSecret: session.client.clientSecret,
|
|
5169
|
+
tokenEndpointAuthMethod: session.client.tokenEndpointAuthMethod,
|
|
5131
5170
|
refreshToken: session.tokens.refreshToken,
|
|
5132
5171
|
resource,
|
|
5133
5172
|
fetch: fetch2,
|
|
@@ -5191,6 +5230,7 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
5191
5230
|
let resolvedClient = null;
|
|
5192
5231
|
try {
|
|
5193
5232
|
resolvedClient = await resolveClient(currentSession, discovery, loopback.redirectUri, fetch2, signal);
|
|
5233
|
+
assertTokenEndpointAuthentication(resolvedClient.client, discovery.authorizationServerMetadata);
|
|
5194
5234
|
const sessionWithoutTokens = {
|
|
5195
5235
|
resource,
|
|
5196
5236
|
authorizationServer: discovery.authorizationServer,
|
|
@@ -5214,6 +5254,7 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
5214
5254
|
tokenEndpoint: requireOwnString(discovery.authorizationServerMetadata, "token_endpoint", "Authorization server metadata"),
|
|
5215
5255
|
clientId: resolvedClient.client.clientId,
|
|
5216
5256
|
clientSecret: resolvedClient.client.clientSecret,
|
|
5257
|
+
tokenEndpointAuthMethod: resolvedClient.client.tokenEndpointAuthMethod,
|
|
5217
5258
|
code,
|
|
5218
5259
|
codeVerifier: verifier,
|
|
5219
5260
|
redirectUri: loopback.redirectUri,
|
|
@@ -5257,6 +5298,9 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
5257
5298
|
if (configuredClient === null) {
|
|
5258
5299
|
throw new Error("OAuth client_id must not be blank");
|
|
5259
5300
|
}
|
|
5301
|
+
assertRegistrationIssuer(configuredClient, discovery.authorizationServer);
|
|
5302
|
+
if (hasExpiredClientSecret(configuredClient, now))
|
|
5303
|
+
throw new Error("OAuth client secret has expired; update the imported registration");
|
|
5260
5304
|
return {
|
|
5261
5305
|
kind: "static",
|
|
5262
5306
|
fromStoredRegistration: false,
|
|
@@ -5271,7 +5315,14 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
5271
5315
|
client: configuredClient
|
|
5272
5316
|
};
|
|
5273
5317
|
}
|
|
5274
|
-
|
|
5318
|
+
let storedClient = await loadRegisteredClient(discovery.authorizationServer);
|
|
5319
|
+
if (storedClient !== null) {
|
|
5320
|
+
assertRegistrationIssuer(storedClient, discovery.authorizationServer);
|
|
5321
|
+
if (hasExpiredClientSecret(storedClient, now)) {
|
|
5322
|
+
await clearRegisteredClient(discovery.authorizationServer);
|
|
5323
|
+
storedClient = null;
|
|
5324
|
+
}
|
|
5325
|
+
}
|
|
5275
5326
|
if (storedClient !== null) {
|
|
5276
5327
|
return {
|
|
5277
5328
|
kind: "dynamic",
|
|
@@ -5280,7 +5331,7 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
5280
5331
|
};
|
|
5281
5332
|
}
|
|
5282
5333
|
if (registrationEndpoint === void 0) {
|
|
5283
|
-
if (existingSession !== null && existingSession.client.clientId.length > 0) {
|
|
5334
|
+
if (existingSession !== null && existingSession.client.clientId.length > 0 && !hasExpiredClientSecret(existingSession.client, now)) {
|
|
5284
5335
|
return {
|
|
5285
5336
|
kind: "dynamic",
|
|
5286
5337
|
fromStoredRegistration: true,
|
|
@@ -5289,7 +5340,7 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
5289
5340
|
}
|
|
5290
5341
|
throw new Error("Authorization server metadata is missing registration_endpoint");
|
|
5291
5342
|
}
|
|
5292
|
-
if (existingSession !== null && existingSession.client.clientId.length > 0) {
|
|
5343
|
+
if (existingSession !== null && existingSession.client.clientId.length > 0 && !hasExpiredClientSecret(existingSession.client, now)) {
|
|
5293
5344
|
const isConfiguredStaticFallback = configuredClient !== null && existingSession.client.clientId === configuredClient.clientId && existingSession.client.clientSecret === configuredClient.clientSecret;
|
|
5294
5345
|
if (!isConfiguredStaticFallback) {
|
|
5295
5346
|
await saveRegisteredClient(discovery.authorizationServer, existingSession.client);
|
|
@@ -5300,7 +5351,11 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
5300
5351
|
};
|
|
5301
5352
|
}
|
|
5302
5353
|
}
|
|
5303
|
-
const
|
|
5354
|
+
const supported = getSupportedTokenAuthMethods(discovery.authorizationServerMetadata);
|
|
5355
|
+
const registrationMethod = requestedTokenMethod ?? (supported === void 0 ? "none" : ["none", "client_secret_basic", "client_secret_post"].find((method) => supported.includes(method)));
|
|
5356
|
+
if (registrationMethod === void 0 || supported !== void 0 && !supported.includes(registrationMethod))
|
|
5357
|
+
throw new Error("Authorization server does not support the requested OAuth token endpoint authentication");
|
|
5358
|
+
const registrationBody = buildClientRegistrationBody(clientMetadata, redirectUri, registrationMethod);
|
|
5304
5359
|
const deadline = AbortSignal.timeout(3e4);
|
|
5305
5360
|
const signal = parentSignal === void 0 ? deadline : AbortSignal.any([parentSignal, deadline]);
|
|
5306
5361
|
const response = await fetchMcpResponse(fetch2, registrationEndpoint, {
|
|
@@ -5314,11 +5369,16 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
5314
5369
|
const payload = await readOAuthJsonObjectResponse(response, signal);
|
|
5315
5370
|
const registration = parseOAuthClientRegistration(payload);
|
|
5316
5371
|
const registeredSecret = getOwnString2(registration, "client_secret");
|
|
5372
|
+
const responseMethod = normalizeOAuthTokenEndpointAuthMethod(getOwnEntry6(registration, "token_endpoint_auth_method")) ?? requestedTokenMethod ?? (supported === void 0 ? void 0 : normalizeOAuthTokenEndpointAuthMethod(registrationMethod));
|
|
5317
5373
|
const registeredClient = {
|
|
5318
5374
|
clientId: registration.client_id.trim(),
|
|
5319
5375
|
...registeredSecret === void 0 ? {} : { clientSecret: registeredSecret.trim() },
|
|
5376
|
+
...responseMethod === void 0 ? {} : { tokenEndpointAuthMethod: responseMethod },
|
|
5320
5377
|
registration
|
|
5321
5378
|
};
|
|
5379
|
+
assertRegistrationIssuer(registeredClient, discovery.authorizationServer);
|
|
5380
|
+
if (hasExpiredClientSecret(registeredClient, now))
|
|
5381
|
+
throw new Error("OAuth client secret has expired in the registration response");
|
|
5322
5382
|
await saveRegisteredClient(discovery.authorizationServer, registeredClient);
|
|
5323
5383
|
return {
|
|
5324
5384
|
kind: "dynamic",
|
|
@@ -5330,7 +5390,7 @@ function createDefaultOAuthClientProvider(options) {
|
|
|
5330
5390
|
return normalizeLoadedSession(await sessionStore.load(resource));
|
|
5331
5391
|
}
|
|
5332
5392
|
async function saveSession(resource, session) {
|
|
5333
|
-
await sessionStore.save(resource, session);
|
|
5393
|
+
await sessionStore.save(resource, structuredClone(session));
|
|
5334
5394
|
}
|
|
5335
5395
|
async function clearSession(resource) {
|
|
5336
5396
|
await sessionStore.clear(resource);
|
|
@@ -5426,6 +5486,19 @@ function normalizeLoadedSession(session) {
|
|
|
5426
5486
|
tokens: normalizeStoredTokens(getOwnEntry6(session, "tokens"))
|
|
5427
5487
|
};
|
|
5428
5488
|
}
|
|
5489
|
+
function normalizeImportedTokens(value, now) {
|
|
5490
|
+
if (!isObjectRecord3(value))
|
|
5491
|
+
return void 0;
|
|
5492
|
+
const absolute = getOwnEntry6(value, "expiresAt");
|
|
5493
|
+
const lifetime = getOwnEntry6(value, "expiresIn");
|
|
5494
|
+
const issuedAt = getOwnEntry6(value, "issuedAt");
|
|
5495
|
+
if (lifetime !== void 0 && (typeof lifetime !== "number" || !Number.isSafeInteger(lifetime) || lifetime < 0))
|
|
5496
|
+
throw new Error("OAuth initial grant has invalid relative expiry");
|
|
5497
|
+
if (issuedAt !== void 0 && (typeof issuedAt !== "number" || !Number.isSafeInteger(issuedAt) || Math.abs(issuedAt) > MAX_JS_DATE_MS3))
|
|
5498
|
+
throw new Error("OAuth initial grant has invalid issuance time");
|
|
5499
|
+
const expiresAt = absolute !== void 0 && absolute !== null ? absolute : lifetime === void 0 ? null : (issuedAt === void 0 ? now() : issuedAt) + lifetime * 1e3;
|
|
5500
|
+
return normalizeStoredTokens({ ...value, expiresAt });
|
|
5501
|
+
}
|
|
5429
5502
|
function normalizeStoredTokens(value) {
|
|
5430
5503
|
if (value === void 0 || !isObjectRecord3(value)) {
|
|
5431
5504
|
return void 0;
|
|
@@ -5466,7 +5539,7 @@ function normalizeConfiguredClient(client) {
|
|
|
5466
5539
|
if (clientId === void 0)
|
|
5467
5540
|
return null;
|
|
5468
5541
|
const clientSecret = normalizeOptionalOAuthString(client.clientSecret) ?? (registration === void 0 ? void 0 : getOwnString2(registration, "client_secret")?.trim());
|
|
5469
|
-
return normalizeStoredOAuthClient({ clientId, clientSecret, registration });
|
|
5542
|
+
return normalizeStoredOAuthClient({ clientId, clientSecret, registration, tokenEndpointAuthMethod: client.tokenEndpointAuthMethod });
|
|
5470
5543
|
}
|
|
5471
5544
|
function normalizeOptionalOAuthString(value) {
|
|
5472
5545
|
if (value === void 0) {
|
|
@@ -5566,12 +5639,39 @@ function assertRequestMatchesResource(requestUrl, resource) {
|
|
|
5566
5639
|
throw new Error(`OAuth request URL ${requestUrl} does not match discovered resource ${resource}`);
|
|
5567
5640
|
}
|
|
5568
5641
|
}
|
|
5569
|
-
function
|
|
5642
|
+
function assertRegistrationIssuer(client, issuer) {
|
|
5643
|
+
const registrationIssuer = client.registration === void 0 ? void 0 : getOwnString2(client.registration, "issuer");
|
|
5644
|
+
if (registrationIssuer !== void 0 && registrationIssuer !== issuer)
|
|
5645
|
+
throw new Error("OAuth client registration issuer does not match the authorization server");
|
|
5646
|
+
}
|
|
5647
|
+
function hasExpiredClientSecret(client, now) {
|
|
5648
|
+
if (client.clientSecret === void 0 || client.tokenEndpointAuthMethod === "none" || client.registration === void 0)
|
|
5649
|
+
return false;
|
|
5650
|
+
const expiry = getOwnEntry6(client.registration, "client_secret_expires_at");
|
|
5651
|
+
return typeof expiry === "number" && expiry !== 0 && expiry <= now() / 1e3;
|
|
5652
|
+
}
|
|
5653
|
+
function getSupportedTokenAuthMethods(metadata) {
|
|
5654
|
+
const value = getOwnEntry6(metadata, "token_endpoint_auth_methods_supported");
|
|
5655
|
+
if (value === void 0)
|
|
5656
|
+
return void 0;
|
|
5657
|
+
if (!Array.isArray(value) || value.length > 128 || value.some((method) => typeof method !== "string"))
|
|
5658
|
+
throw new Error("Invalid OAuth token endpoint authentication metadata");
|
|
5659
|
+
return value;
|
|
5660
|
+
}
|
|
5661
|
+
function assertTokenEndpointAuthentication(client, metadata) {
|
|
5662
|
+
const method = client.tokenEndpointAuthMethod ?? (client.clientSecret === void 0 ? "none" : "client_secret_post");
|
|
5663
|
+
if (method !== "none" && client.clientSecret === void 0)
|
|
5664
|
+
throw new Error("OAuth token endpoint authentication requires a client secret");
|
|
5665
|
+
const supported = getSupportedTokenAuthMethods(metadata);
|
|
5666
|
+
if (supported !== void 0 && !supported.includes(method))
|
|
5667
|
+
throw new Error("Authorization server does not support the requested OAuth token endpoint authentication");
|
|
5668
|
+
}
|
|
5669
|
+
function buildClientRegistrationBody(metadata, redirectUri, tokenEndpointAuthMethod) {
|
|
5570
5670
|
const body = {
|
|
5571
5671
|
redirect_uris: [redirectUri],
|
|
5572
5672
|
grant_types: ["authorization_code", "refresh_token"],
|
|
5573
5673
|
response_types: ["code"],
|
|
5574
|
-
token_endpoint_auth_method:
|
|
5674
|
+
token_endpoint_auth_method: tokenEndpointAuthMethod
|
|
5575
5675
|
};
|
|
5576
5676
|
const clientName = metadata === void 0 ? void 0 : getOwnString2(metadata, "clientName");
|
|
5577
5677
|
const scope = metadata === void 0 ? void 0 : getOwnString2(metadata, "scope");
|