@frontmcp/auth 1.11.2 → 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.
- package/README.md +2 -2
- package/authorities/authorities.context.d.ts.map +1 -1
- package/authorities/authorities.engine.d.ts +1 -1
- package/authorities/authorities.engine.d.ts.map +1 -1
- package/authorities/authorities.evaluator.d.ts.map +1 -1
- package/authorities/authorities.profiles.d.ts +1 -1
- package/authorities/authorities.profiles.d.ts.map +1 -1
- package/authorities/authorities.registry.d.ts +1 -1
- package/authorities/authorities.registry.d.ts.map +1 -1
- package/authorities/authorities.scope-mapping.d.ts +1 -1
- package/authorities/authorities.scope-mapping.d.ts.map +1 -1
- package/authorities/authorities.types.d.ts +4 -0
- package/authorities/authorities.types.d.ts.map +1 -1
- package/authorization/orchestrated.accessor.d.ts.map +1 -1
- package/authorization/public.authorization.d.ts.map +1 -1
- package/authorization/transparent.authorization.d.ts +1 -1
- package/authorization/transparent.authorization.d.ts.map +1 -1
- package/browser/authorities/authorities.context.mjs +3 -1
- package/browser/authorities/authorities.engine.mjs +32 -33
- package/browser/authorities/authorities.evaluator.mjs +42 -16
- package/browser/authorization/dcr-client.registry.mjs +1 -1
- package/browser/authorization/public.authorization.mjs +1 -1
- package/browser/authorization/transparent.authorization.mjs +1 -1
- package/browser/cimd/cimd-client-assertion.mjs +108 -0
- package/browser/cimd/cimd-redis.cache.mjs +5 -3
- package/browser/cimd/cimd.errors.mjs +11 -0
- package/browser/cimd/cimd.service.mjs +127 -31
- package/browser/cimd/cimd.types.mjs +15 -10
- package/browser/cimd/index.mjs +13 -1
- package/browser/errors/auth-internal.error.mjs +3 -2
- package/browser/errors/auth-internal.errors.mjs +8 -1
- package/browser/errors/index.mjs +2 -0
- package/browser/index.mjs +21 -1
- package/browser/jwks/index.mjs +2 -0
- package/browser/jwks/jwks.service.mjs +22 -103
- package/browser/jwks/local-token-keys.mjs +155 -0
- package/browser/options/public.schema.mjs +7 -4
- package/browser/options/shared.schemas.mjs +38 -13
- package/browser/session/federated-auth.session.mjs +2 -0
- package/browser/session/incremental-auth-ticket.mjs +24 -1
- package/browser/session/index.mjs +4 -0
- package/browser/session/redis-session.store.mjs +3 -3
- package/browser/session/session-crypto.mjs +4 -4
- package/browser/session/storage/storage-authorization-vault.mjs +3 -1
- package/browser/session/token.vault.mjs +1 -1
- package/browser/session/utils/index.mjs +9 -1
- package/browser/session/utils/session-crypto.utils.mjs +4 -0
- package/browser/ui/index.mjs +2 -0
- package/browser/ui/templates.mjs +65 -11
- package/browser/upstream/index.mjs +6 -0
- package/browser/upstream/upstream-client-registration.mjs +273 -0
- package/cimd/cimd-client-assertion.d.ts +43 -0
- package/cimd/cimd-client-assertion.d.ts.map +1 -0
- package/cimd/cimd-redis.cache.d.ts +2 -2
- package/cimd/cimd-redis.cache.d.ts.map +1 -1
- package/cimd/cimd.cache.d.ts +1 -1
- package/cimd/cimd.cache.d.ts.map +1 -1
- package/cimd/cimd.errors.d.ts +14 -0
- package/cimd/cimd.errors.d.ts.map +1 -1
- package/cimd/cimd.service.d.ts +37 -2
- package/cimd/cimd.service.d.ts.map +1 -1
- package/cimd/cimd.types.d.ts +6 -0
- package/cimd/cimd.types.d.ts.map +1 -1
- package/cimd/index.d.ts +3 -2
- package/cimd/index.d.ts.map +1 -1
- package/errors/auth-internal.error.d.ts +4 -5
- package/errors/auth-internal.error.d.ts.map +1 -1
- package/errors/auth-internal.errors.d.ts +8 -0
- package/errors/auth-internal.errors.d.ts.map +1 -1
- package/errors/index.d.ts +1 -1
- package/errors/index.d.ts.map +1 -1
- package/esm/index.mjs +54 -14
- package/esm/package.json +5 -5
- package/index.d.ts +12 -10
- package/index.d.ts.map +1 -1
- package/index.js +58 -18
- package/jwks/index.d.ts +2 -0
- package/jwks/index.d.ts.map +1 -1
- package/jwks/jwks.service.d.ts +12 -12
- package/jwks/jwks.service.d.ts.map +1 -1
- package/jwks/jwks.types.d.ts +2 -0
- package/jwks/jwks.types.d.ts.map +1 -1
- package/jwks/local-token-keys.d.ts +41 -0
- package/jwks/local-token-keys.d.ts.map +1 -0
- package/options/interfaces.d.ts +64 -15
- package/options/interfaces.d.ts.map +1 -1
- package/options/public.schema.d.ts.map +1 -1
- package/options/shared.schemas.d.ts.map +1 -1
- package/package.json +5 -5
- package/session/authorization.store.d.ts +9 -2
- package/session/authorization.store.d.ts.map +1 -1
- package/session/federated-auth.session.d.ts +8 -0
- package/session/federated-auth.session.d.ts.map +1 -1
- package/session/incremental-auth-ticket.d.ts +10 -0
- package/session/incremental-auth-ticket.d.ts.map +1 -1
- package/session/index.d.ts +2 -2
- package/session/index.d.ts.map +1 -1
- package/session/redis-session.store.d.ts +1 -1
- package/session/redis-session.store.d.ts.map +1 -1
- package/session/session-crypto.d.ts.map +1 -1
- package/session/storage/in-memory-authorization-vault.d.ts.map +1 -1
- package/session/storage/storage-authorization-vault.d.ts +2 -2
- package/session/storage/storage-authorization-vault.d.ts.map +1 -1
- package/session/storage/storage-token-store.d.ts +2 -2
- package/session/storage/storage-token-store.d.ts.map +1 -1
- package/session/token.refresh.d.ts.map +1 -1
- package/session/utils/index.d.ts +1 -1
- package/session/utils/index.d.ts.map +1 -1
- package/session/utils/session-crypto.utils.d.ts +5 -0
- package/session/utils/session-crypto.utils.d.ts.map +1 -1
- package/ui/index.d.ts +2 -2
- package/ui/index.d.ts.map +1 -1
- package/ui/templates.d.ts +33 -0
- package/ui/templates.d.ts.map +1 -1
- package/upstream/index.d.ts +3 -0
- package/upstream/index.d.ts.map +1 -0
- package/upstream/upstream-client-registration.d.ts +50 -0
- package/upstream/upstream-client-registration.d.ts.map +1 -0
- package/vault/auth-providers.accessor.d.ts +1 -1
- package/vault/auth-providers.accessor.d.ts.map +1 -1
- package/vault/auth-providers.registry.d.ts +1 -1
- package/vault/auth-providers.registry.d.ts.map +1 -1
- package/vault/credential-cache.d.ts +1 -1
- package/vault/credential-cache.d.ts.map +1 -1
- package/vault/credential-loaders/eager-loader.d.ts +2 -2
- 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 {
|
|
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
|
|
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
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
369
|
+
throw new CimdResponseTooLargeError(url, this.networkConfig.maxResponseSizeBytes, length);
|
|
274
370
|
}
|
|
275
371
|
}
|
|
276
|
-
const text = await this.readResponseWithLimit(response, this.networkConfig.maxResponseSizeBytes,
|
|
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(
|
|
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(
|
|
388
|
+
throw new CimdFetchError(url, "Request timeout", {
|
|
293
389
|
originalError: error
|
|
294
390
|
});
|
|
295
391
|
}
|
|
296
|
-
throw new CimdFetchError(
|
|
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
|
|
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
|
|
53
|
+
* Inline JWKS, for `private_key_jwt`. Not together with `jwks_uri`.
|
|
48
54
|
*/
|
|
49
|
-
jwks:
|
|
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
|
-
*
|
|
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
|
};
|
package/browser/cimd/index.mjs
CHANGED
|
@@ -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 {
|
|
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
|
|
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,
|
package/browser/errors/index.mjs
CHANGED
|
@@ -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,
|
package/browser/jwks/index.mjs
CHANGED
|
@@ -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
|