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.
@@ -18,7 +18,7 @@
18
18
  },
19
19
  {
20
20
  "name": "tiny-http-mcp-server",
21
- "version": "0.1.33",
21
+ "version": "0.1.35",
22
22
  "license": "MIT"
23
23
  },
24
24
  {
@@ -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` if unknown),
110
- and optional `scope`. A fresh imported token is used only for its resource.
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: normalizeStoredTokens(options.initialGrant.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
- const storedClient = await loadRegisteredClient(discovery.authorizationServer);
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 registrationBody = buildClientRegistrationBody(clientMetadata, redirectUri);
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 buildClientRegistrationBody(metadata, redirectUri) {
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: "none"
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,2 @@
1
+ import type { OAuthTokenEndpointAuthMethod } from "./types.js";
2
+ export declare function normalizeOAuthTokenEndpointAuthMethod(value: unknown): OAuthTokenEndpointAuthMethod | undefined;
@@ -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 body = new URLSearchParams({
73
- client_id: input.clientId,
74
- ...input.params
75
- });
76
- if (input.clientSecret !== undefined) {
77
- body.set("client_secret", input.clientSecret);
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: StoredOAuthTokens;
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: StoredOAuthTokens;
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 body = new URLSearchParams({
4793
- client_id: input.clientId,
4794
- ...input.params
4795
- });
4796
- if (input.clientSecret !== void 0) {
4797
- body.set("client_secret", input.clientSecret);
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: normalizeStoredTokens(options.initialGrant.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
- const storedClient = await loadRegisteredClient(discovery.authorizationServer);
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 registrationBody = buildClientRegistrationBody(clientMetadata, redirectUri);
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 buildClientRegistrationBody(metadata, redirectUri) {
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: "none"
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");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tiny-http-mcp-server",
3
- "version": "0.1.33",
3
+ "version": "0.1.35",
4
4
  "description": "Minimal MCP server over HTTP built on tiny-stdio-mcp-server",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",