@frontmcp/auth 1.11.1 → 1.11.3

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.
Files changed (126) hide show
  1. package/README.md +2 -2
  2. package/authorities/authorities.context.d.ts.map +1 -1
  3. package/authorities/authorities.engine.d.ts +1 -1
  4. package/authorities/authorities.engine.d.ts.map +1 -1
  5. package/authorities/authorities.evaluator.d.ts.map +1 -1
  6. package/authorities/authorities.profiles.d.ts +1 -1
  7. package/authorities/authorities.profiles.d.ts.map +1 -1
  8. package/authorities/authorities.registry.d.ts +1 -1
  9. package/authorities/authorities.registry.d.ts.map +1 -1
  10. package/authorities/authorities.scope-mapping.d.ts +1 -1
  11. package/authorities/authorities.scope-mapping.d.ts.map +1 -1
  12. package/authorities/authorities.types.d.ts +4 -0
  13. package/authorities/authorities.types.d.ts.map +1 -1
  14. package/authorization/orchestrated.accessor.d.ts.map +1 -1
  15. package/authorization/public.authorization.d.ts.map +1 -1
  16. package/authorization/transparent.authorization.d.ts +1 -1
  17. package/authorization/transparent.authorization.d.ts.map +1 -1
  18. package/browser/authorities/authorities.context.mjs +3 -1
  19. package/browser/authorities/authorities.engine.mjs +32 -33
  20. package/browser/authorities/authorities.evaluator.mjs +42 -16
  21. package/browser/authorization/dcr-client.registry.mjs +1 -1
  22. package/browser/authorization/public.authorization.mjs +1 -1
  23. package/browser/authorization/transparent.authorization.mjs +1 -1
  24. package/browser/cimd/cimd-client-assertion.mjs +108 -0
  25. package/browser/cimd/cimd-redis.cache.mjs +5 -3
  26. package/browser/cimd/cimd.errors.mjs +11 -0
  27. package/browser/cimd/cimd.service.mjs +127 -31
  28. package/browser/cimd/cimd.types.mjs +15 -10
  29. package/browser/cimd/index.mjs +13 -1
  30. package/browser/errors/auth-internal.error.mjs +3 -2
  31. package/browser/errors/auth-internal.errors.mjs +8 -1
  32. package/browser/errors/index.mjs +2 -0
  33. package/browser/index.mjs +21 -1
  34. package/browser/jwks/index.mjs +2 -0
  35. package/browser/jwks/jwks.service.mjs +22 -103
  36. package/browser/jwks/local-token-keys.mjs +155 -0
  37. package/browser/options/public.schema.mjs +7 -4
  38. package/browser/options/shared.schemas.mjs +38 -13
  39. package/browser/session/federated-auth.session.mjs +2 -0
  40. package/browser/session/incremental-auth-ticket.mjs +24 -1
  41. package/browser/session/index.mjs +4 -0
  42. package/browser/session/redis-session.store.mjs +3 -3
  43. package/browser/session/session-crypto.mjs +4 -4
  44. package/browser/session/storage/storage-authorization-vault.mjs +3 -1
  45. package/browser/session/token.vault.mjs +1 -1
  46. package/browser/session/utils/index.mjs +9 -1
  47. package/browser/session/utils/session-crypto.utils.mjs +4 -0
  48. package/browser/ui/index.mjs +2 -0
  49. package/browser/ui/templates.mjs +65 -11
  50. package/browser/upstream/index.mjs +6 -0
  51. package/browser/upstream/upstream-client-registration.mjs +273 -0
  52. package/cimd/cimd-client-assertion.d.ts +43 -0
  53. package/cimd/cimd-client-assertion.d.ts.map +1 -0
  54. package/cimd/cimd-redis.cache.d.ts +2 -2
  55. package/cimd/cimd-redis.cache.d.ts.map +1 -1
  56. package/cimd/cimd.cache.d.ts +1 -1
  57. package/cimd/cimd.cache.d.ts.map +1 -1
  58. package/cimd/cimd.errors.d.ts +14 -0
  59. package/cimd/cimd.errors.d.ts.map +1 -1
  60. package/cimd/cimd.service.d.ts +37 -2
  61. package/cimd/cimd.service.d.ts.map +1 -1
  62. package/cimd/cimd.types.d.ts +6 -0
  63. package/cimd/cimd.types.d.ts.map +1 -1
  64. package/cimd/index.d.ts +3 -2
  65. package/cimd/index.d.ts.map +1 -1
  66. package/errors/auth-internal.error.d.ts +4 -5
  67. package/errors/auth-internal.error.d.ts.map +1 -1
  68. package/errors/auth-internal.errors.d.ts +8 -0
  69. package/errors/auth-internal.errors.d.ts.map +1 -1
  70. package/errors/index.d.ts +1 -1
  71. package/errors/index.d.ts.map +1 -1
  72. package/esm/index.mjs +54 -14
  73. package/esm/package.json +5 -5
  74. package/index.d.ts +12 -10
  75. package/index.d.ts.map +1 -1
  76. package/index.js +58 -18
  77. package/jwks/index.d.ts +2 -0
  78. package/jwks/index.d.ts.map +1 -1
  79. package/jwks/jwks.service.d.ts +12 -12
  80. package/jwks/jwks.service.d.ts.map +1 -1
  81. package/jwks/jwks.types.d.ts +2 -0
  82. package/jwks/jwks.types.d.ts.map +1 -1
  83. package/jwks/local-token-keys.d.ts +41 -0
  84. package/jwks/local-token-keys.d.ts.map +1 -0
  85. package/options/interfaces.d.ts +64 -15
  86. package/options/interfaces.d.ts.map +1 -1
  87. package/options/public.schema.d.ts.map +1 -1
  88. package/options/shared.schemas.d.ts.map +1 -1
  89. package/package.json +5 -5
  90. package/session/authorization.store.d.ts +9 -2
  91. package/session/authorization.store.d.ts.map +1 -1
  92. package/session/federated-auth.session.d.ts +8 -0
  93. package/session/federated-auth.session.d.ts.map +1 -1
  94. package/session/incremental-auth-ticket.d.ts +10 -0
  95. package/session/incremental-auth-ticket.d.ts.map +1 -1
  96. package/session/index.d.ts +2 -2
  97. package/session/index.d.ts.map +1 -1
  98. package/session/redis-session.store.d.ts +1 -1
  99. package/session/redis-session.store.d.ts.map +1 -1
  100. package/session/session-crypto.d.ts.map +1 -1
  101. package/session/storage/in-memory-authorization-vault.d.ts.map +1 -1
  102. package/session/storage/storage-authorization-vault.d.ts +2 -2
  103. package/session/storage/storage-authorization-vault.d.ts.map +1 -1
  104. package/session/storage/storage-token-store.d.ts +2 -2
  105. package/session/storage/storage-token-store.d.ts.map +1 -1
  106. package/session/token.refresh.d.ts.map +1 -1
  107. package/session/utils/index.d.ts +1 -1
  108. package/session/utils/index.d.ts.map +1 -1
  109. package/session/utils/session-crypto.utils.d.ts +5 -0
  110. package/session/utils/session-crypto.utils.d.ts.map +1 -1
  111. package/ui/index.d.ts +2 -2
  112. package/ui/index.d.ts.map +1 -1
  113. package/ui/templates.d.ts +33 -0
  114. package/ui/templates.d.ts.map +1 -1
  115. package/upstream/index.d.ts +3 -0
  116. package/upstream/index.d.ts.map +1 -0
  117. package/upstream/upstream-client-registration.d.ts +50 -0
  118. package/upstream/upstream-client-registration.d.ts.map +1 -0
  119. package/vault/auth-providers.accessor.d.ts +1 -1
  120. package/vault/auth-providers.accessor.d.ts.map +1 -1
  121. package/vault/auth-providers.registry.d.ts +1 -1
  122. package/vault/auth-providers.registry.d.ts.map +1 -1
  123. package/vault/credential-cache.d.ts +1 -1
  124. package/vault/credential-cache.d.ts.map +1 -1
  125. package/vault/credential-loaders/eager-loader.d.ts +2 -2
  126. package/vault/credential-loaders/eager-loader.d.ts.map +1 -1
@@ -124,12 +124,23 @@ var CimdResponseTooLargeError = class extends CimdError {
124
124
  return "Client metadata document is too large";
125
125
  }
126
126
  };
127
+ var CimdClientAssertionError = class extends CimdError {
128
+ reason;
129
+ constructor(clientIdUrl, reason) {
130
+ super(`Client assertion refused for ${clientIdUrl}: ${reason}`, "CIMD_CLIENT_ASSERTION_INVALID", 401, clientIdUrl);
131
+ this.reason = reason;
132
+ }
133
+ getPublicMessage() {
134
+ return "Client authentication failed";
135
+ }
136
+ };
127
137
  var CimdDisabledError = class extends CimdError {
128
138
  constructor() {
129
139
  super("CIMD (Client ID Metadata Documents) is disabled on this server", "CIMD_DISABLED", 400);
130
140
  }
131
141
  };
132
142
  export {
143
+ CimdClientAssertionError,
133
144
  CimdClientIdMismatchError,
134
145
  CimdDisabledError,
135
146
  CimdError,
@@ -1,12 +1,15 @@
1
1
  // libs/auth/src/cimd/cimd.service.ts
2
2
  import { isRedirectResponse } from "@frontmcp/utils";
3
- import { createCimdCache } from "./cimd.cache.mjs";
3
+ import { verifyClientAssertion } from "./cimd-client-assertion.mjs";
4
+ import { createCimdCache, extractCacheHeaders, parseCacheHeaders } from "./cimd.cache.mjs";
4
5
  import {
6
+ CimdClientAssertionError,
5
7
  CimdClientIdMismatchError,
6
8
  CimdFetchError,
7
9
  CimdResponseTooLargeError,
8
10
  CimdSecurityError,
9
11
  CimdValidationError,
12
+ InvalidClientIdUrlError,
10
13
  RedirectUriMismatchError
11
14
  } from "./cimd.errors.mjs";
12
15
  import { noopLogger } from "./cimd.logger.mjs";
@@ -15,15 +18,19 @@ import {
15
18
  cimdConfigSchema,
16
19
  cimdNetworkConfigSchema,
17
20
  cimdSecurityConfigSchema,
21
+ clientJwksSchema,
18
22
  clientMetadataDocumentSchema
19
23
  } from "./cimd.types.mjs";
20
24
  import { assertHostNotSsrf, hasOnlyLocalhostRedirectUris, isCimdClientId, validateClientIdUrl } from "./cimd.validator.mjs";
25
+ var MAX_CACHED_KEY_SETS = 500;
26
+ var KEY_SET_REFRESH_COOLDOWN_MS = 3e4;
21
27
  var CimdService = class {
22
28
  config;
23
29
  cacheConfig;
24
30
  securityConfig;
25
31
  networkConfig;
26
32
  cacheBackend;
33
+ keySets = /* @__PURE__ */ new Map();
27
34
  logger;
28
35
  /**
29
36
  * Whether CIMD is enabled.
@@ -152,6 +159,91 @@ var CimdService = class {
152
159
  throw new RedirectUriMismatchError(metadata.client_id, redirectUri, metadata.redirect_uris);
153
160
  }
154
161
  }
162
+ /**
163
+ * Check that this server can honor the client as its document declares it: it authenticates at the token
164
+ * endpoint with `none`, or with `private_key_jwt` and exactly one of `jwks` / `jwks_uri` (a public document holds
165
+ * no shared secret, so `client_secret_*` is refused), and it uses the authorization code grant with
166
+ * `response_type=code`.
167
+ *
168
+ * @param metadata - The client's metadata document
169
+ * @throws CimdValidationError naming each declaration this server cannot honor
170
+ */
171
+ assertSupportedClient(metadata) {
172
+ const problems = [];
173
+ const method = metadata.token_endpoint_auth_method;
174
+ if (method === "private_key_jwt") {
175
+ problems.push(...this.keySourceProblems(metadata));
176
+ } else if (method !== "none") {
177
+ problems.push(`token_endpoint_auth_method "${method}" is not supported, only "none" or "private_key_jwt"`);
178
+ }
179
+ if (!metadata.grant_types.includes("authorization_code")) {
180
+ problems.push('grant_types must include "authorization_code"');
181
+ }
182
+ if (!metadata.response_types.includes("code")) {
183
+ problems.push('response_types must include "code"');
184
+ }
185
+ if (problems.length > 0) {
186
+ throw new CimdValidationError(metadata.client_id, problems);
187
+ }
188
+ }
189
+ /**
190
+ * Authenticate a `private_key_jwt` client by its assertion (RFC 7523): see {@link verifyClientAssertion}.
191
+ *
192
+ * @param metadata - The client's metadata document, which declares `private_key_jwt`
193
+ * @param check - The token request's assertion, the audiences it may name, and the `jti` store
194
+ * @throws CimdClientAssertionError when the assertion is missing or refused
195
+ * @throws CimdFetchError, CimdResponseTooLargeError or CimdSecurityError when `jwks_uri` can't be fetched
196
+ */
197
+ async verifyClientAssertion(metadata, check) {
198
+ await verifyClientAssertion({
199
+ ...check,
200
+ clientId: metadata.client_id,
201
+ keys: (refresh) => this.clientKeys(metadata, refresh)
202
+ });
203
+ }
204
+ /**
205
+ * The client's keys: its inline `jwks`, or the key set at its `jwks_uri`, fetched like the document itself
206
+ * (SSRF guard, redirect policy, timeout, size limit) and cached for the TTL its cache headers give, within
207
+ * `cache.minTtlMs` and `cache.maxTtlMs`. With `refresh`, a cached key set at least
208
+ * {@link KEY_SET_REFRESH_COOLDOWN_MS} old is fetched again.
209
+ */
210
+ async clientKeys(metadata, refresh) {
211
+ if (metadata.jwks) return metadata.jwks;
212
+ const jwksUri = metadata.jwks_uri;
213
+ if (!jwksUri) throw new CimdClientAssertionError(metadata.client_id, "no_keys");
214
+ const now = Date.now();
215
+ const cached = this.keySets.get(jwksUri);
216
+ const reusable = cached && cached.expiresAt > now && (!refresh || now - cached.fetchedAt < KEY_SET_REFRESH_COOLDOWN_MS);
217
+ if (reusable) return cached.keys;
218
+ validateClientIdUrl(jwksUri, this.securityConfig);
219
+ const fetched = await this.fetchJson(jwksUri);
220
+ if (fetched.notModified) throw new CimdFetchError(jwksUri, "304 Not Modified to an unconditional request");
221
+ const parsed = clientJwksSchema.safeParse(fetched.document);
222
+ if (!parsed.success) throw new CimdValidationError(jwksUri, ["jwks_uri did not return a JSON Web Key Set"]);
223
+ const { ttlMs } = parseCacheHeaders(extractCacheHeaders(fetched.headers), this.cacheConfig);
224
+ this.keySets.delete(jwksUri);
225
+ this.keySets.set(jwksUri, { keys: parsed.data, fetchedAt: now, expiresAt: now + ttlMs });
226
+ if (this.keySets.size > MAX_CACHED_KEY_SETS) {
227
+ const oldest = this.keySets.keys().next().value;
228
+ if (oldest !== void 0) this.keySets.delete(oldest);
229
+ }
230
+ return parsed.data;
231
+ }
232
+ /** Why a `private_key_jwt` document's keys can't be used: neither or both of `jwks` / `jwks_uri`, or a bad URI. */
233
+ keySourceProblems(metadata) {
234
+ if (!metadata.jwks === !metadata.jwks_uri) {
235
+ return ['token_endpoint_auth_method "private_key_jwt" needs exactly one of "jwks" or "jwks_uri"'];
236
+ }
237
+ if (!metadata.jwks_uri) return [];
238
+ try {
239
+ validateClientIdUrl(metadata.jwks_uri, this.securityConfig);
240
+ return [];
241
+ } catch (error) {
242
+ if (error instanceof InvalidClientIdUrlError) return [`jwks_uri is not allowed: ${error.reason}`];
243
+ if (error instanceof CimdSecurityError) return [`jwks_uri is not allowed: ${error.securityReason}`];
244
+ throw error;
245
+ }
246
+ }
155
247
  /**
156
248
  * Clear the cache for a specific client or all clients.
157
249
  *
@@ -177,24 +269,39 @@ var CimdService = class {
177
269
  };
178
270
  }
179
271
  /**
180
- * Fetch a metadata document from a CIMD URL.
272
+ * Fetch a metadata document from a CIMD URL, revalidating a cached one with a conditional request.
273
+ */
274
+ async fetchMetadataDocument(clientId) {
275
+ const cache = await this.cache();
276
+ const conditionalHeaders = await cache.getConditionalHeaders(clientId);
277
+ const fetched = await this.fetchJson(clientId, conditionalHeaders);
278
+ if (!fetched.notModified) return fetched;
279
+ const staleEntry = await cache.getStale(clientId);
280
+ if (!staleEntry) {
281
+ throw new CimdFetchError(clientId, "304 Not Modified but no cached entry", { httpStatus: 304 });
282
+ }
283
+ await cache.revalidate(clientId, fetched.headers);
284
+ this.logger.debug(`CIMD document not modified: ${clientId}`);
285
+ return { document: staleEntry.document, headers: fetched.headers };
286
+ }
287
+ /**
288
+ * Fetch a JSON document (a client metadata document or a client's `jwks_uri`) with the SSRF guard on every hop,
289
+ * the redirect policy, the timeout and the size limit. `url` names the document in every error.
181
290
  *
182
291
  * Following the JwksService.fetchJson() pattern.
183
292
  */
184
- async fetchMetadataDocument(clientId) {
293
+ async fetchJson(url, conditionalHeaders) {
185
294
  const controller = new AbortController();
186
295
  const timer = setTimeout(() => controller.abort(), this.networkConfig.timeoutMs);
187
296
  try {
188
- const cache = await this.cache();
189
- const conditionalHeaders = await cache.getConditionalHeaders(clientId);
190
- const originalOrigin = new URL(clientId).origin;
297
+ const originalOrigin = new URL(url).origin;
191
298
  const maxRedirects = this.networkConfig.maxRedirects;
192
- let currentUrl = clientId;
299
+ let currentUrl = url;
193
300
  let redirectCount = 0;
194
301
  let isFirstRequest = true;
195
302
  while (true) {
196
303
  if (this.securityConfig.blockPrivateIPs && !this.securityConfig.allowInsecureForTesting) {
197
- await assertHostNotSsrf(new URL(currentUrl).hostname, clientId);
304
+ await assertHostNotSsrf(new URL(currentUrl).hostname, url);
198
305
  }
199
306
  const headers = {
200
307
  Accept: "application/json"
@@ -209,23 +316,12 @@ var CimdService = class {
209
316
  redirect: "manual"
210
317
  });
211
318
  if (response.status === 304) {
212
- const staleEntry = await cache.getStale(clientId);
213
- if (staleEntry) {
214
- await cache.revalidate(clientId, response.headers);
215
- this.logger.debug(`CIMD document not modified: ${clientId}`);
216
- return {
217
- document: staleEntry.document,
218
- headers: response.headers
219
- };
220
- }
221
- throw new CimdFetchError(clientId, "304 Not Modified but no cached entry", {
222
- httpStatus: 304
223
- });
319
+ return { notModified: true, headers: response.headers };
224
320
  }
225
321
  if (isRedirectResponse(response)) {
226
322
  const location = response.headers.get("location");
227
323
  if (!location) {
228
- throw new CimdFetchError(clientId, "Redirect response missing Location header", {
324
+ throw new CimdFetchError(url, "Redirect response missing Location header", {
229
325
  httpStatus: response.status
230
326
  });
231
327
  }
@@ -233,7 +329,7 @@ var CimdService = class {
233
329
  const redirectPolicy = this.networkConfig.redirectPolicy;
234
330
  if (redirectPolicy === "deny") {
235
331
  throw new CimdFetchError(
236
- clientId,
332
+ url,
237
333
  `CIMD fetch redirected to "${nextUrl}" but redirects are disabled. Host the metadata at the client_id URL or set auth.cimd.network.redirectPolicy to "same-origin" or "allow".`,
238
334
  { httpStatus: response.status }
239
335
  );
@@ -242,7 +338,7 @@ var CimdService = class {
242
338
  const nextOrigin = new URL(nextUrl).origin;
243
339
  if (nextOrigin !== originalOrigin) {
244
340
  throw new CimdFetchError(
245
- clientId,
341
+ url,
246
342
  `CIMD fetch redirected to "${nextUrl}" which is not the same origin as "${originalOrigin}". Host the metadata at the client_id URL or set auth.cimd.network.redirectPolicy to "allow".`,
247
343
  { httpStatus: response.status }
248
344
  );
@@ -252,7 +348,7 @@ var CimdService = class {
252
348
  redirectCount += 1;
253
349
  if (redirectCount > maxRedirects) {
254
350
  throw new CimdFetchError(
255
- clientId,
351
+ url,
256
352
  `CIMD fetch exceeded max redirects (${maxRedirects}). Host the metadata at the client_id URL or increase auth.cimd.network.maxRedirects.`
257
353
  );
258
354
  }
@@ -262,7 +358,7 @@ var CimdService = class {
262
358
  continue;
263
359
  }
264
360
  if (!response.ok) {
265
- throw new CimdFetchError(clientId, `HTTP ${response.status} ${response.statusText}`, {
361
+ throw new CimdFetchError(url, `HTTP ${response.status} ${response.statusText}`, {
266
362
  httpStatus: response.status
267
363
  });
268
364
  }
@@ -270,30 +366,30 @@ var CimdService = class {
270
366
  if (contentLength) {
271
367
  const length = parseInt(contentLength, 10);
272
368
  if (!isNaN(length) && length > this.networkConfig.maxResponseSizeBytes) {
273
- throw new CimdResponseTooLargeError(clientId, this.networkConfig.maxResponseSizeBytes, length);
369
+ throw new CimdResponseTooLargeError(url, this.networkConfig.maxResponseSizeBytes, length);
274
370
  }
275
371
  }
276
- const text = await this.readResponseWithLimit(response, this.networkConfig.maxResponseSizeBytes, clientId);
372
+ const text = await this.readResponseWithLimit(response, this.networkConfig.maxResponseSizeBytes, url);
277
373
  let document;
278
374
  try {
279
375
  document = JSON.parse(text);
280
376
  } catch (e) {
281
- throw new CimdFetchError(clientId, "Invalid JSON response", {
377
+ throw new CimdFetchError(url, "Invalid JSON response", {
282
378
  originalError: e instanceof Error ? e : new Error(String(e))
283
379
  });
284
380
  }
285
- return { document, headers: response.headers };
381
+ return { notModified: false, document, headers: response.headers };
286
382
  }
287
383
  } catch (error) {
288
384
  if (error instanceof CimdFetchError || error instanceof CimdResponseTooLargeError || error instanceof CimdSecurityError) {
289
385
  throw error;
290
386
  }
291
387
  if (error instanceof Error && error.name === "AbortError") {
292
- throw new CimdFetchError(clientId, "Request timeout", {
388
+ throw new CimdFetchError(url, "Request timeout", {
293
389
  originalError: error
294
390
  });
295
391
  }
296
- throw new CimdFetchError(clientId, error instanceof Error ? error.message : "Unknown error", {
392
+ throw new CimdFetchError(url, error instanceof Error ? error.message : "Unknown error", {
297
393
  originalError: error instanceof Error ? error : void 0
298
394
  });
299
395
  } finally {
@@ -1,5 +1,8 @@
1
1
  // libs/auth/src/cimd/cimd.types.ts
2
2
  import { internalZ as z } from "@frontmcp/lazy-zod";
3
+ var clientJwksSchema = z.object({
4
+ keys: z.array(z.record(z.string(), z.unknown()))
5
+ });
3
6
  var clientMetadataDocumentSchema = z.object({
4
7
  // REQUIRED per CIMD spec
5
8
  /**
@@ -17,15 +20,17 @@ var clientMetadataDocumentSchema = z.object({
17
20
  redirect_uris: z.array(z.string().url()).min(1),
18
21
  // OPTIONAL per RFC 7591
19
22
  /**
20
- * Token endpoint authentication method.
23
+ * Token endpoint authentication method. FrontMCP's authorization server accepts `none` (a public client) and
24
+ * `private_key_jwt` (a client assertion signed with a key from `jwks` / `jwks_uri`, RFC 7523). A public document
25
+ * holds no shared secret, so `client_secret_*` is refused.
21
26
  * @default 'none'
22
27
  */
23
28
  token_endpoint_auth_method: z.enum(["none", "client_secret_basic", "client_secret_post", "private_key_jwt"]).default("none"),
24
29
  /**
25
- * OAuth grant types the client can use.
26
- * @default ['authorization_code']
30
+ * OAuth grant types the client can use. Without `refresh_token`, the client gets no refresh token.
31
+ * @default ['authorization_code', 'refresh_token']
27
32
  */
28
- grant_types: z.array(z.string()).default(["authorization_code"]),
33
+ grant_types: z.array(z.string()).default(["authorization_code", "refresh_token"]),
29
34
  /**
30
35
  * OAuth response types the client can request.
31
36
  * @default ['code']
@@ -40,15 +45,14 @@ var clientMetadataDocumentSchema = z.object({
40
45
  */
41
46
  logo_uri: z.string().url().optional(),
42
47
  /**
43
- * URL of the client's JWKS (for private_key_jwt).
48
+ * URL of the client's JWKS, for `private_key_jwt`. Fetched with the same protections as the document itself,
49
+ * and cached. Not together with `jwks`.
44
50
  */
45
51
  jwks_uri: z.string().url().optional(),
46
52
  /**
47
- * Inline JWKS (for private_key_jwt).
53
+ * Inline JWKS, for `private_key_jwt`. Not together with `jwks_uri`.
48
54
  */
49
- jwks: z.object({
50
- keys: z.array(z.record(z.string(), z.unknown()))
51
- }).optional(),
55
+ jwks: clientJwksSchema.optional(),
52
56
  /**
53
57
  * URL of the client's terms of service.
54
58
  */
@@ -58,7 +62,7 @@ var clientMetadataDocumentSchema = z.object({
58
62
  */
59
63
  policy_uri: z.string().url().optional(),
60
64
  /**
61
- * Requested OAuth scopes.
65
+ * Space-separated scopes the client may be granted (RFC 7591); a request for others is narrowed to these.
62
66
  */
63
67
  scope: z.string().optional(),
64
68
  /**
@@ -224,5 +228,6 @@ export {
224
228
  cimdNetworkConfigSchema,
225
229
  cimdRedisCacheConfigSchema,
226
230
  cimdSecurityConfigSchema,
231
+ clientJwksSchema,
227
232
  clientMetadataDocumentSchema
228
233
  };
@@ -1,6 +1,7 @@
1
1
  // libs/auth/src/cimd/index.ts
2
2
  import { noopLogger } from "./cimd.logger.mjs";
3
3
  import {
4
+ clientJwksSchema,
4
5
  clientMetadataDocumentSchema,
5
6
  cimdCacheConfigSchema,
6
7
  cimdRedisCacheConfigSchema,
@@ -17,8 +18,14 @@ import {
17
18
  CimdSecurityError,
18
19
  RedirectUriMismatchError,
19
20
  CimdResponseTooLargeError,
20
- CimdDisabledError
21
+ CimdDisabledError,
22
+ CimdClientAssertionError
21
23
  } from "./cimd.errors.mjs";
24
+ import {
25
+ CLIENT_ASSERTION_TYPE_JWT_BEARER,
26
+ CLIENT_ASSERTION_SIGNING_ALGORITHMS,
27
+ MAX_CLIENT_ASSERTION_LIFETIME_SECONDS
28
+ } from "./cimd-client-assertion.mjs";
22
29
  import {
23
30
  isCimdClientId,
24
31
  validateClientIdUrl,
@@ -34,6 +41,9 @@ import {
34
41
  import { RedisCimdCache } from "./cimd-redis.cache.mjs";
35
42
  import { CimdService } from "./cimd.service.mjs";
36
43
  export {
44
+ CLIENT_ASSERTION_SIGNING_ALGORITHMS,
45
+ CLIENT_ASSERTION_TYPE_JWT_BEARER,
46
+ CimdClientAssertionError,
37
47
  CimdClientIdMismatchError,
38
48
  CimdDisabledError,
39
49
  CimdError,
@@ -44,6 +54,7 @@ export {
44
54
  CimdValidationError,
45
55
  InMemoryCimdCache,
46
56
  InvalidClientIdUrlError,
57
+ MAX_CLIENT_ASSERTION_LIFETIME_SECONDS,
47
58
  RedirectUriMismatchError,
48
59
  RedisCimdCache,
49
60
  checkSsrfProtection,
@@ -52,6 +63,7 @@ export {
52
63
  cimdNetworkConfigSchema,
53
64
  cimdRedisCacheConfigSchema,
54
65
  cimdSecurityConfigSchema,
66
+ clientJwksSchema,
55
67
  clientMetadataDocumentSchema,
56
68
  createCimdCache,
57
69
  extractCacheHeaders,
@@ -1,12 +1,12 @@
1
1
  // libs/auth/src/errors/auth-internal.error.ts
2
- import { randomBytes, bytesToHex } from "@frontmcp/utils";
2
+ import { bytesToHex, randomBytes } from "@frontmcp/utils";
3
3
  var AuthInternalError = class extends Error {
4
4
  /**
5
5
  * Unique error ID for tracking in logs.
6
6
  */
7
7
  errorId;
8
8
  /**
9
- * Whether this error should expose details to the client.
9
+ * Whether the client sees the message: true only for an error that names a setting and nothing secret.
10
10
  */
11
11
  isPublic = false;
12
12
  /**
@@ -28,6 +28,7 @@ var AuthInternalError = class extends Error {
28
28
  * Get the public-facing error message.
29
29
  */
30
30
  getPublicMessage() {
31
+ if (this.isPublic) return this.message;
31
32
  return `Internal auth error. Please contact support with error ID: ${this.errorId}`;
32
33
  }
33
34
  /**
@@ -58,16 +58,22 @@ var JwtSecretWeakError = class extends AuthInternalError {
58
58
  );
59
59
  }
60
60
  };
61
+ var SigningKeyInvalidError = class extends AuthInternalError {
62
+ constructor(message) {
63
+ super(message, "SIGNING_KEY_INVALID");
64
+ }
65
+ };
61
66
  var CredentialProviderAlreadyRegisteredError = class extends AuthInternalError {
62
67
  constructor(name) {
63
68
  super(`Credential provider "${name}" is already registered`, "CREDENTIAL_PROVIDER_ALREADY_REGISTERED");
64
69
  }
65
70
  };
66
71
  var AuthProvidersNotConfiguredError = class extends AuthInternalError {
72
+ isPublic = true;
67
73
  constructor(providerName) {
68
74
  const register = "Register it in @FrontMcp({ authProvidersVault: { providers: [...] } }).";
69
75
  super(
70
- providerName === void 0 ? "Auth providers are not configured: this server registers no auth providers." : `Auth provider "${providerName}" is not configured: this server registers no auth providers. ${register}`,
76
+ providerName === void 0 ? "Auth providers are not configured: this server registers no auth providers. Register them in @FrontMcp({ authProvidersVault: { providers: [...] } })." : `Auth provider "${providerName}" is not configured: this server registers no auth providers. ${register}`,
71
77
  "AUTH_PROVIDERS_NOT_CONFIGURED"
72
78
  );
73
79
  }
@@ -146,6 +152,7 @@ export {
146
152
  SecureStoreConfigError,
147
153
  SessionIdEmptyError,
148
154
  SessionSecretRequiredError,
155
+ SigningKeyInvalidError,
149
156
  TokenLeakDetectedError,
150
157
  TokenNotAvailableError,
151
158
  TokenStoreRequiredError,
@@ -10,6 +10,7 @@ import {
10
10
  TokenLeakDetectedError,
11
11
  JwtSecretRequiredError,
12
12
  JwtSecretWeakError,
13
+ SigningKeyInvalidError,
13
14
  SessionSecretRequiredError,
14
15
  CredentialProviderAlreadyRegisteredError,
15
16
  AuthProvidersNotConfiguredError,
@@ -45,6 +46,7 @@ export {
45
46
  SecureStoreConfigError,
46
47
  SessionIdEmptyError,
47
48
  SessionSecretRequiredError,
49
+ SigningKeyInvalidError,
48
50
  TokenLeakDetectedError,
49
51
  TokenNotAvailableError,
50
52
  TokenStoreRequiredError,
package/browser/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // libs/auth/src/index.ts
2
- import { JwksService, trimSlash, normalizeIssuer, decodeJwtPayloadSafe } from "./jwks/index.mjs";
2
+ import { JwksService, LocalTokenKeys, trimSlash, normalizeIssuer, decodeJwtPayloadSafe } from "./jwks/index.mjs";
3
3
  import {
4
4
  CDN,
5
5
  DEFAULT_THEME,
@@ -15,6 +15,7 @@ import {
15
15
  buildConsentPage,
16
16
  buildIncrementalAuthPage,
17
17
  buildFederatedLoginPage,
18
+ buildFederatedStepPage,
18
19
  buildToolConsentPage,
19
20
  buildLoginPage,
20
21
  buildConnectPage,
@@ -108,6 +109,7 @@ import {
108
109
  decryptSessionJson,
109
110
  safeDecrypt,
110
111
  resetCachedKey,
112
+ isSessionSecretConfigured,
111
113
  RedisSessionStore,
112
114
  VercelKvSessionStore,
113
115
  InMemoryOrchestratedTokenStore,
@@ -143,6 +145,7 @@ import {
143
145
  DEFAULT_RESUME_TTL_MS,
144
146
  signIncrementalAuthTicket,
145
147
  verifyIncrementalAuthTicket,
148
+ openIncrementalAuthTicketProfile,
146
149
  DEFAULT_INCREMENTAL_TICKET_TTL_MS,
147
150
  CREDENTIALS_ACCESSOR,
148
151
  CredentialsAccessorImpl,
@@ -183,6 +186,7 @@ import {
183
186
  TokenLeakDetectedError,
184
187
  JwtSecretRequiredError,
185
188
  JwtSecretWeakError,
189
+ SigningKeyInvalidError,
186
190
  SessionSecretRequiredError,
187
191
  CredentialProviderAlreadyRegisteredError,
188
192
  AuthProvidersNotConfiguredError,
@@ -262,6 +266,10 @@ import {
262
266
  RedirectUriMismatchError,
263
267
  CimdResponseTooLargeError,
264
268
  CimdDisabledError,
269
+ CimdClientAssertionError,
270
+ CLIENT_ASSERTION_TYPE_JWT_BEARER,
271
+ CLIENT_ASSERTION_SIGNING_ALGORITHMS,
272
+ MAX_CLIENT_ASSERTION_LIFETIME_SECONDS,
265
273
  isCimdClientId,
266
274
  validateClientIdUrl,
267
275
  checkSsrfProtection,
@@ -271,6 +279,7 @@ import {
271
279
  parseCacheHeaders,
272
280
  CimdService
273
281
  } from "./cimd/index.mjs";
282
+ import { resolveUpstreamClient, UpstreamClientRegistrationError } from "./upstream/index.mjs";
274
283
  import { FrontMcpAuthContextImpl, buildAuthContext } from "./context/index.mjs";
275
284
  export { AuthoritiesContextBuilder, AuthoritiesEngine, AuthoritiesEvaluatorRegistry, AuthoritiesProfileRegistry, AuthorityDeniedError, abacConditionSchema, abacOperatorSchema, abacPolicySchema, authoritiesClaimsMappingSchema, authoritiesConfigSchema, authoritiesMetadataSchema, authoritiesPolicySchema, findAuthoritiesProfileProblems, findAuthoritiesRuleProblems, isAnonymousSubject, rbacPermissionsPolicySchema, rbacRolesPolicySchema, rebacPolicySchema, resolveRequiredScopes, resourceIdRefSchema } from "./authorities/index.mjs";
276
285
  import {
@@ -306,7 +315,10 @@ export {
306
315
  AuthorizationBase,
307
316
  BUILT_IN_AUTH_PAGE_CSP,
308
317
  CDN,
318
+ CLIENT_ASSERTION_SIGNING_ALGORITHMS,
319
+ CLIENT_ASSERTION_TYPE_JWT_BEARER,
309
320
  CREDENTIALS_ACCESSOR,
321
+ CimdClientAssertionError,
310
322
  CimdClientIdMismatchError,
311
323
  CimdDisabledError,
312
324
  CimdError,
@@ -346,7 +358,9 @@ export {
346
358
  JwtSecretRequiredError,
347
359
  JwtSecretWeakError,
348
360
  LazyCredentialLoader,
361
+ LocalTokenKeys,
349
362
  LoopbackTokenAuth,
363
+ MAX_CLIENT_ASSERTION_LIFETIME_SECONDS,
350
364
  NoProviderIdError,
351
365
  NullOrchestratedAuthAccessor,
352
366
  ORCHESTRATED_AUTH_ACCESSOR,
@@ -370,6 +384,7 @@ export {
370
384
  SessionIdEmptyError,
371
385
  SessionRateLimiter,
372
386
  SessionSecretRequiredError,
387
+ SigningKeyInvalidError,
373
388
  StorageAuthorizationStore,
374
389
  StorageAuthorizationVault,
375
390
  StorageConsentStore,
@@ -385,6 +400,7 @@ export {
385
400
  TransportIdGenerator,
386
401
  TypedStorage,
387
402
  UNIDENTIFIED_SESSION_PREFIX,
403
+ UpstreamClientRegistrationError,
388
404
  VaultEncryption,
389
405
  VaultLoadError,
390
406
  VaultNotFoundError,
@@ -418,6 +434,7 @@ export {
418
434
  buildCredentialResumeUrl,
419
435
  buildErrorPage,
420
436
  buildFederatedLoginPage,
437
+ buildFederatedStepPage,
421
438
  buildIncrementalAuthPage,
422
439
  buildInsufficientScopeHeader,
423
440
  buildInvalidRequestHeader,
@@ -499,6 +516,7 @@ export {
499
516
  isPersistentTokenStorage,
500
517
  isRedisTokenStorage,
501
518
  isSessionComplete,
519
+ isSessionSecretConfigured,
502
520
  isSharedServerNonce,
503
521
  isSignedSession,
504
522
  isSoonExpiring,
@@ -514,6 +532,7 @@ export {
514
532
  noopLogger,
515
533
  normalizeIssuer,
516
534
  oauthCredentialSchema,
535
+ openIncrementalAuthTicketProfile,
517
536
  openPendingLogin,
518
537
  parseCacheHeaders,
519
538
  parseWwwAuthenticate,
@@ -532,6 +551,7 @@ export {
532
551
  renderLocalLoginPage,
533
552
  renderToHtml,
534
553
  resetCachedKey,
554
+ resolveUpstreamClient,
535
555
  resourceScopesFor,
536
556
  safeDecrypt,
537
557
  scopesFromClaims,
@@ -1,8 +1,10 @@
1
1
  // libs/auth/src/jwks/index.ts
2
2
  import { JwksService } from "./jwks.service.mjs";
3
+ import { LocalTokenKeys } from "./local-token-keys.mjs";
3
4
  import { trimSlash, normalizeIssuer, decodeJwtPayloadSafe } from "./jwks.utils.mjs";
4
5
  export {
5
6
  JwksService,
7
+ LocalTokenKeys,
6
8
  decodeJwtPayloadSafe,
7
9
  normalizeIssuer,
8
10
  trimSlash