@frontmcp/auth 1.9.1 → 1.9.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/authorities/authorities.context.d.ts.map +1 -1
- package/authorities/authorities.evaluator.d.ts.map +1 -1
- package/authorization/orchestrated.authorization.d.ts +10 -2
- package/authorization/orchestrated.authorization.d.ts.map +1 -1
- package/cimd/cimd.service.d.ts +13 -1
- package/cimd/cimd.service.d.ts.map +1 -1
- package/context/frontmcp-auth-context.factory.d.ts +7 -5
- package/context/frontmcp-auth-context.factory.d.ts.map +1 -1
- package/context/frontmcp-auth-context.impl.d.ts +2 -1
- package/context/frontmcp-auth-context.impl.d.ts.map +1 -1
- package/detection/auth-provider-detection.d.ts +2 -0
- package/detection/auth-provider-detection.d.ts.map +1 -1
- package/esm/index.mjs +534 -116
- package/esm/package.json +5 -5
- package/fetch/fetch-credential-middleware.d.ts +11 -1
- package/fetch/fetch-credential-middleware.d.ts.map +1 -1
- package/fetch/index.d.ts +2 -2
- package/fetch/index.d.ts.map +1 -1
- package/index.d.ts +3 -3
- package/index.d.ts.map +1 -1
- package/index.js +534 -119
- package/jwks/jwks.service.d.ts +17 -1
- package/jwks/jwks.service.d.ts.map +1 -1
- package/package.json +5 -5
- package/session/authorization.store.d.ts +6 -0
- package/session/authorization.store.d.ts.map +1 -1
- package/session/orchestrated-token.crypto.d.ts +11 -0
- package/session/orchestrated-token.crypto.d.ts.map +1 -1
- package/session/orchestrated-token.store.d.ts +16 -1
- package/session/orchestrated-token.store.d.ts.map +1 -1
- package/session/storage-orchestrated-token.store.d.ts +10 -0
- package/session/storage-orchestrated-token.store.d.ts.map +1 -1
- package/session/utils/auth-token.utils.d.ts.map +1 -1
- package/utils/index.d.ts +1 -0
- package/utils/index.d.ts.map +1 -1
- package/utils/token-scopes.utils.d.ts +11 -0
- package/utils/token-scopes.utils.d.ts.map +1 -0
package/esm/index.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
var __frontmcpModule = typeof process !== "undefined" && typeof process.getBuiltinModule === "function" ? process.getBuiltinModule("module") : undefined;
|
|
2
2
|
const require = __frontmcpModule ? __frontmcpModule.createRequire(import.meta.url || 'file:///') : undefined;
|
|
3
3
|
// __frontmcpCreateRequire
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
6
|
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
6
7
|
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
@@ -16,6 +17,249 @@ var __esm = (fn, res, err) => function __init() {
|
|
|
16
17
|
throw err = [e], e;
|
|
17
18
|
}
|
|
18
19
|
};
|
|
20
|
+
var __export = (target, all) => {
|
|
21
|
+
for (var name in all)
|
|
22
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// libs/auth/src/cimd/cimd-redis.cache.ts
|
|
26
|
+
var cimd_redis_cache_exports = {};
|
|
27
|
+
__export(cimd_redis_cache_exports, {
|
|
28
|
+
RedisCimdCache: () => RedisCimdCache
|
|
29
|
+
});
|
|
30
|
+
import { RedisStorageAdapter as RedisStorageAdapter3 } from "@frontmcp/utils";
|
|
31
|
+
import { sha256Hex as sha256Hex5 } from "@frontmcp/utils";
|
|
32
|
+
var RedisCimdCache;
|
|
33
|
+
var init_cimd_redis_cache = __esm({
|
|
34
|
+
"libs/auth/src/cimd/cimd-redis.cache.ts"() {
|
|
35
|
+
"use strict";
|
|
36
|
+
init_cimd_cache();
|
|
37
|
+
RedisCimdCache = class {
|
|
38
|
+
redis;
|
|
39
|
+
keyPrefix;
|
|
40
|
+
config;
|
|
41
|
+
constructor(config) {
|
|
42
|
+
if (!config.redis) {
|
|
43
|
+
throw new Error("Redis configuration is required for RedisCimdCache");
|
|
44
|
+
}
|
|
45
|
+
this.config = {
|
|
46
|
+
defaultTtlMs: config.defaultTtlMs ?? 36e5,
|
|
47
|
+
maxTtlMs: config.maxTtlMs ?? 864e5,
|
|
48
|
+
minTtlMs: config.minTtlMs ?? 6e4
|
|
49
|
+
};
|
|
50
|
+
this.keyPrefix = config.redis.keyPrefix ?? "cimd:";
|
|
51
|
+
const redisOptions = {};
|
|
52
|
+
if (config.redis.url) {
|
|
53
|
+
redisOptions.url = config.redis.url;
|
|
54
|
+
} else if (config.redis.host) {
|
|
55
|
+
redisOptions.config = {
|
|
56
|
+
host: config.redis.host,
|
|
57
|
+
port: config.redis.port,
|
|
58
|
+
password: config.redis.password,
|
|
59
|
+
db: config.redis.db,
|
|
60
|
+
tls: config.redis.tls
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
this.redis = new RedisStorageAdapter3(redisOptions);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Connect to Redis.
|
|
67
|
+
* Must be called before using any cache operations.
|
|
68
|
+
*/
|
|
69
|
+
async connect() {
|
|
70
|
+
await this.redis.connect();
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Generate a Redis key for a client ID.
|
|
74
|
+
* Uses SHA-256 hash to handle URLs with special characters.
|
|
75
|
+
*/
|
|
76
|
+
cacheKey(clientId) {
|
|
77
|
+
const hash = sha256Hex5(clientId);
|
|
78
|
+
return `${this.keyPrefix}${hash}`;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Get a cached entry by client_id.
|
|
82
|
+
*
|
|
83
|
+
* @param clientId - The client_id URL
|
|
84
|
+
* @returns The cached entry if valid, or undefined
|
|
85
|
+
*/
|
|
86
|
+
async get(clientId) {
|
|
87
|
+
const key = this.cacheKey(clientId);
|
|
88
|
+
const data = await this.redis.get(key);
|
|
89
|
+
if (!data) {
|
|
90
|
+
return void 0;
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const entry = JSON.parse(data);
|
|
94
|
+
if (entry.expiresAt < Date.now()) {
|
|
95
|
+
return void 0;
|
|
96
|
+
}
|
|
97
|
+
return entry;
|
|
98
|
+
} catch {
|
|
99
|
+
await this.redis.delete(key);
|
|
100
|
+
return void 0;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Get a stale entry for conditional revalidation.
|
|
105
|
+
*
|
|
106
|
+
* @param clientId - The client_id URL
|
|
107
|
+
* @returns The stale entry (even if expired), or undefined if not cached
|
|
108
|
+
*/
|
|
109
|
+
async getStale(clientId) {
|
|
110
|
+
const key = this.cacheKey(clientId);
|
|
111
|
+
const data = await this.redis.get(key);
|
|
112
|
+
if (!data) {
|
|
113
|
+
return void 0;
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
return JSON.parse(data);
|
|
117
|
+
} catch {
|
|
118
|
+
return void 0;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Store a document in the cache.
|
|
123
|
+
*
|
|
124
|
+
* @param clientId - The client_id URL
|
|
125
|
+
* @param document - The metadata document
|
|
126
|
+
* @param headers - HTTP response headers
|
|
127
|
+
*/
|
|
128
|
+
async set(clientId, document, headers) {
|
|
129
|
+
const cacheHeaders = extractCacheHeaders(headers);
|
|
130
|
+
const { ttlMs, etag, lastModified } = parseCacheHeaders(cacheHeaders, this.config);
|
|
131
|
+
const now = Date.now();
|
|
132
|
+
const entry = {
|
|
133
|
+
document,
|
|
134
|
+
expiresAt: now + ttlMs,
|
|
135
|
+
etag,
|
|
136
|
+
lastModified,
|
|
137
|
+
cachedAt: now
|
|
138
|
+
};
|
|
139
|
+
const key = this.cacheKey(clientId);
|
|
140
|
+
const value = JSON.stringify(entry);
|
|
141
|
+
const redisTtlSeconds = Math.ceil((ttlMs + this.config.maxTtlMs * 2) / 1e3);
|
|
142
|
+
await this.redis.set(key, value, { ttlSeconds: redisTtlSeconds });
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Update an existing cache entry (after 304 Not Modified).
|
|
146
|
+
*
|
|
147
|
+
* @param clientId - The client_id URL
|
|
148
|
+
* @param headers - New HTTP headers with updated cache directives
|
|
149
|
+
*/
|
|
150
|
+
async revalidate(clientId, headers) {
|
|
151
|
+
const key = this.cacheKey(clientId);
|
|
152
|
+
const data = await this.redis.get(key);
|
|
153
|
+
if (!data) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
const existing = JSON.parse(data);
|
|
158
|
+
const cacheHeaders = extractCacheHeaders(headers);
|
|
159
|
+
const { ttlMs, etag, lastModified } = parseCacheHeaders(cacheHeaders, this.config);
|
|
160
|
+
existing.expiresAt = Date.now() + ttlMs;
|
|
161
|
+
if (etag) existing.etag = etag;
|
|
162
|
+
if (lastModified) existing.lastModified = lastModified;
|
|
163
|
+
const value = JSON.stringify(existing);
|
|
164
|
+
const redisTtlSeconds = Math.ceil((ttlMs + this.config.maxTtlMs * 2) / 1e3);
|
|
165
|
+
await this.redis.set(key, value, { ttlSeconds: redisTtlSeconds });
|
|
166
|
+
return true;
|
|
167
|
+
} catch {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Delete a cache entry.
|
|
173
|
+
*
|
|
174
|
+
* @param clientId - The client_id URL
|
|
175
|
+
* @returns true if an entry was deleted
|
|
176
|
+
*/
|
|
177
|
+
async delete(clientId) {
|
|
178
|
+
const key = this.cacheKey(clientId);
|
|
179
|
+
return this.redis.delete(key);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Get conditional request headers for a cached entry.
|
|
183
|
+
*
|
|
184
|
+
* @param clientId - The client_id URL
|
|
185
|
+
* @returns Headers for conditional request, or undefined if not cached
|
|
186
|
+
*/
|
|
187
|
+
async getConditionalHeaders(clientId) {
|
|
188
|
+
const key = this.cacheKey(clientId);
|
|
189
|
+
const data = await this.redis.get(key);
|
|
190
|
+
if (!data) {
|
|
191
|
+
return void 0;
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
const entry = JSON.parse(data);
|
|
195
|
+
const headers = {};
|
|
196
|
+
if (entry.etag) {
|
|
197
|
+
headers["If-None-Match"] = entry.etag;
|
|
198
|
+
}
|
|
199
|
+
if (entry.lastModified) {
|
|
200
|
+
headers["If-Modified-Since"] = entry.lastModified;
|
|
201
|
+
}
|
|
202
|
+
return Object.keys(headers).length > 0 ? headers : void 0;
|
|
203
|
+
} catch {
|
|
204
|
+
return void 0;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Clear all cached entries.
|
|
209
|
+
* Uses Redis SCAN to find and delete all keys with our prefix.
|
|
210
|
+
*/
|
|
211
|
+
async clear() {
|
|
212
|
+
const keys = await this.redis.keys(`${this.keyPrefix}*`);
|
|
213
|
+
if (keys.length > 0) {
|
|
214
|
+
await this.redis.mdelete(keys);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Get the number of cached entries.
|
|
219
|
+
* Uses Redis SCAN to count keys with our prefix.
|
|
220
|
+
*/
|
|
221
|
+
async size() {
|
|
222
|
+
const keys = await this.redis.keys(`${this.keyPrefix}*`);
|
|
223
|
+
return keys.length;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Remove expired entries.
|
|
227
|
+
*
|
|
228
|
+
* Note: Redis handles expiration automatically via TTL.
|
|
229
|
+
* This method is primarily for explicit cleanup of entries that are
|
|
230
|
+
* well past their HTTP cache expiration but still within Redis TTL.
|
|
231
|
+
*
|
|
232
|
+
* @returns Number of entries removed
|
|
233
|
+
*/
|
|
234
|
+
async cleanup() {
|
|
235
|
+
const now = Date.now();
|
|
236
|
+
let removed = 0;
|
|
237
|
+
const keys = await this.redis.keys(`${this.keyPrefix}*`);
|
|
238
|
+
for (const key of keys) {
|
|
239
|
+
const data = await this.redis.get(key);
|
|
240
|
+
if (!data) continue;
|
|
241
|
+
try {
|
|
242
|
+
const entry = JSON.parse(data);
|
|
243
|
+
if (entry.expiresAt + this.config.maxTtlMs * 2 < now) {
|
|
244
|
+
await this.redis.delete(key);
|
|
245
|
+
removed++;
|
|
246
|
+
}
|
|
247
|
+
} catch {
|
|
248
|
+
await this.redis.delete(key);
|
|
249
|
+
removed++;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return removed;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Close the Redis connection.
|
|
256
|
+
*/
|
|
257
|
+
async close() {
|
|
258
|
+
await this.redis.disconnect();
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
});
|
|
19
263
|
|
|
20
264
|
// libs/auth/src/cimd/cimd.cache.ts
|
|
21
265
|
function extractCacheHeaders(headers) {
|
|
@@ -82,6 +326,19 @@ function parseCacheControlHeader(value) {
|
|
|
82
326
|
}
|
|
83
327
|
return result;
|
|
84
328
|
}
|
|
329
|
+
async function createCimdCache(config) {
|
|
330
|
+
const cacheType = config?.type ?? "memory";
|
|
331
|
+
if (cacheType === "redis") {
|
|
332
|
+
if (!config?.redis) {
|
|
333
|
+
throw new Error('Redis configuration is required when cache type is "redis"');
|
|
334
|
+
}
|
|
335
|
+
const { RedisCimdCache: RedisCimdCache2 } = await Promise.resolve().then(() => (init_cimd_redis_cache(), cimd_redis_cache_exports));
|
|
336
|
+
const cache = new RedisCimdCache2(config);
|
|
337
|
+
await cache.connect();
|
|
338
|
+
return cache;
|
|
339
|
+
}
|
|
340
|
+
return new InMemoryCimdCache(config);
|
|
341
|
+
}
|
|
85
342
|
var InMemoryCimdCache;
|
|
86
343
|
var init_cimd_cache = __esm({
|
|
87
344
|
"libs/auth/src/cimd/cimd.cache.ts"() {
|
|
@@ -620,6 +877,7 @@ var TRANSPARENT_JWT_ALGS = [
|
|
|
620
877
|
];
|
|
621
878
|
var MAX_JWKS_FETCH_BYTES = 1048576;
|
|
622
879
|
var MAX_JWKS_FETCH_HOPS = 3;
|
|
880
|
+
var UNKNOWN_KID_REFETCH_COOLDOWN_MS = 6e4;
|
|
623
881
|
var MISSING_EXP_ERROR = 'missing required "exp" claim';
|
|
624
882
|
function isMissingExpError(error) {
|
|
625
883
|
if (typeof error !== "object" || error === null) return false;
|
|
@@ -640,6 +898,8 @@ var JwksService = class {
|
|
|
640
898
|
orchestratorKey;
|
|
641
899
|
// Provider JWKS cache (providerId -> jwks + fetchedAt)
|
|
642
900
|
providerJwks = /* @__PURE__ */ new Map();
|
|
901
|
+
// The last key refetch an unknown `kid` started, per provider (see refetchForUnknownKid)
|
|
902
|
+
unknownKidRefetches = /* @__PURE__ */ new Map();
|
|
643
903
|
// Track if key has been initialized (for async loading)
|
|
644
904
|
keyInitialized = false;
|
|
645
905
|
// Promise guard to prevent concurrent key generation
|
|
@@ -700,6 +960,9 @@ var JwksService = class {
|
|
|
700
960
|
let jwks;
|
|
701
961
|
try {
|
|
702
962
|
jwks = await this.getJwksForProvider(p);
|
|
963
|
+
if (kid && !p.jwks?.keys?.length && !jwks?.keys?.some((key) => key.kid === kid)) {
|
|
964
|
+
jwks = await this.refetchForUnknownKid(p) ?? jwks;
|
|
965
|
+
}
|
|
703
966
|
if (!jwks?.keys?.length) continue;
|
|
704
967
|
const JWKS = createLocalJWKSet(jwks);
|
|
705
968
|
const issuerConstraint = this.issuerConstraintFor(p);
|
|
@@ -867,7 +1130,8 @@ var JwksService = class {
|
|
|
867
1130
|
* 1) inline jwks (if provided) → cache & return
|
|
868
1131
|
* 2) cached & fresh (TTL) → return
|
|
869
1132
|
* 3) explicit jwksUri → fetch, cache, return
|
|
870
|
-
* 4)
|
|
1133
|
+
* 4) `<issuer>/.well-known/jwks.json` → fetch, cache, return
|
|
1134
|
+
* 5) discover jwks_uri → the issuer's OAuth authorization server
|
|
871
1135
|
* metadata (RFC 8414), else its OpenID configuration (OpenID Connect
|
|
872
1136
|
* Discovery 1.0 §4, where Google, Okta, Auth0 and Entra publish it),
|
|
873
1137
|
* then fetch jwks_uri, cache, return
|
|
@@ -881,11 +1145,42 @@ var JwksService = class {
|
|
|
881
1145
|
if (cached && Date.now() - cached.fetchedAt < this.opts.providerJwksTtlMs) {
|
|
882
1146
|
return cached.jwks;
|
|
883
1147
|
}
|
|
1148
|
+
const fetched = await this.fetchProviderJwks(ref);
|
|
1149
|
+
if (fetched) return fetched;
|
|
1150
|
+
const maxStaleMs = Math.max(this.opts.providerJwksTtlMs * 4, 6e4);
|
|
1151
|
+
if (cached && Date.now() - cached.fetchedAt < maxStaleMs) {
|
|
1152
|
+
return cached.jwks;
|
|
1153
|
+
}
|
|
1154
|
+
return void 0;
|
|
1155
|
+
}
|
|
1156
|
+
/**
|
|
1157
|
+
* Fetch a provider's keys again for a token whose `kid` the cached keys don't have (a key the
|
|
1158
|
+
* provider started using since), ahead of the cache's TTL. At most one refetch per provider per
|
|
1159
|
+
* {@link UNKNOWN_KID_REFETCH_COOLDOWN_MS}: tokens that arrive meanwhile wait for the one in flight or
|
|
1160
|
+
* use its result, so a flood of tokens with made-up `kid`s can't hammer the provider. Only freshly
|
|
1161
|
+
* fetched keys are returned: a failed refetch never revives cached keys past the stale window.
|
|
1162
|
+
*/
|
|
1163
|
+
refetchForUnknownKid(ref) {
|
|
1164
|
+
let refetch = this.unknownKidRefetches.get(ref.id);
|
|
1165
|
+
if (!refetch || Date.now() - refetch.startedAt >= UNKNOWN_KID_REFETCH_COOLDOWN_MS) {
|
|
1166
|
+
refetch = { startedAt: Date.now(), done: this.fetchProviderJwks(ref) };
|
|
1167
|
+
this.unknownKidRefetches.set(ref.id, refetch);
|
|
1168
|
+
}
|
|
1169
|
+
return refetch.done;
|
|
1170
|
+
}
|
|
1171
|
+
/**
|
|
1172
|
+
* Fetch a provider's keys, and cache them: from `jwksUri`, else the conventional location, else
|
|
1173
|
+
* the `jwks_uri` of the issuer's OAuth or OpenID metadata. Every URL goes through the same scheme
|
|
1174
|
+
* and SSRF checks (`fetchJson`), `jwks_uri` included.
|
|
1175
|
+
*/
|
|
1176
|
+
async fetchProviderJwks(ref) {
|
|
884
1177
|
if (ref.jwksUri) {
|
|
885
1178
|
const fromUri = await this.tryFetchJwks(ref.id, ref.jwksUri);
|
|
886
1179
|
if (fromUri?.keys?.length) return fromUri;
|
|
887
1180
|
}
|
|
888
1181
|
const issuer = trimSlash(ref.issuerUrl);
|
|
1182
|
+
const fromWellKnown = await this.tryFetchJwks(ref.id, `${issuer}/.well-known/jwks.json`);
|
|
1183
|
+
if (fromWellKnown?.keys?.length) return fromWellKnown;
|
|
889
1184
|
for (const document of ["oauth-authorization-server", "openid-configuration"]) {
|
|
890
1185
|
const meta = await this.tryFetchAsMeta(`${issuer}/.well-known/${document}`);
|
|
891
1186
|
const uri = meta && typeof meta === "object" && meta["jwks_uri"] ? String(meta["jwks_uri"]) : void 0;
|
|
@@ -894,10 +1189,6 @@ var JwksService = class {
|
|
|
894
1189
|
if (fromMeta?.keys?.length) return fromMeta;
|
|
895
1190
|
}
|
|
896
1191
|
}
|
|
897
|
-
const maxStaleMs = Math.max(this.opts.providerJwksTtlMs * 4, 6e4);
|
|
898
|
-
if (cached && Date.now() - cached.fetchedAt < maxStaleMs) {
|
|
899
|
-
return cached.jwks;
|
|
900
|
-
}
|
|
901
1192
|
return void 0;
|
|
902
1193
|
}
|
|
903
1194
|
// ===========================================================================
|
|
@@ -1864,7 +2155,8 @@ function buildRefreshTokenRecord(params) {
|
|
|
1864
2155
|
customClaims: params.customClaims,
|
|
1865
2156
|
federatedLoginUsed: params.federatedLoginUsed,
|
|
1866
2157
|
selectedProviderIds: params.selectedProviderIds,
|
|
1867
|
-
skippedProviderIds: params.skippedProviderIds
|
|
2158
|
+
skippedProviderIds: params.skippedProviderIds,
|
|
2159
|
+
providerTokensId: params.providerTokensId
|
|
1868
2160
|
};
|
|
1869
2161
|
}
|
|
1870
2162
|
function verifyPkce(codeVerifier, challenge) {
|
|
@@ -3935,7 +4227,8 @@ function deriveTypedUser(claims) {
|
|
|
3935
4227
|
...claims,
|
|
3936
4228
|
iss: extractClaimValue(claims, "iss", isString) ?? "",
|
|
3937
4229
|
sid: extractClaimValue(claims, "sid", isString),
|
|
3938
|
-
|
|
4230
|
+
// RFC 9068 §2.2: a client-credentials token names the client as its subject; some IdPs only put it in client_id / azp
|
|
4231
|
+
sub: extractClaimValue(claims, "sub", isString) ?? extractClaimValue(claims, "client_id", isString) ?? extractClaimValue(claims, "azp", isString) ?? "",
|
|
3939
4232
|
exp: extractClaimValue(claims, "exp", isNumber),
|
|
3940
4233
|
iat: extractClaimValue(claims, "iat", isNumber),
|
|
3941
4234
|
aud: extractClaimValue(claims, "aud", isStringOrStringArray),
|
|
@@ -4881,7 +5174,49 @@ var VercelKvSessionStore = class {
|
|
|
4881
5174
|
};
|
|
4882
5175
|
|
|
4883
5176
|
// libs/auth/src/session/orchestrated-token.store.ts
|
|
4884
|
-
import {
|
|
5177
|
+
import { decryptAesGcm as decryptAesGcm5, encryptAesGcm as encryptAesGcm5, hkdfSha256 as hkdfSha2564, randomBytes as randomBytes8 } from "@frontmcp/utils";
|
|
5178
|
+
|
|
5179
|
+
// libs/auth/src/session/orchestrated-token.crypto.ts
|
|
5180
|
+
import { decryptAesGcm as decryptAesGcm4, encryptAesGcm as encryptAesGcm4, hkdfSha256 as hkdfSha2563, randomBytes as randomBytes7 } from "@frontmcp/utils";
|
|
5181
|
+
function recordDropAt(record) {
|
|
5182
|
+
if (!record.refreshToken) return record.expiresAt;
|
|
5183
|
+
return Math.max(record.expiresAt ?? 0, record.updatedAt + REFRESH_TOKEN_TTL_MS);
|
|
5184
|
+
}
|
|
5185
|
+
function hasLiveAccessToken(record, refreshSkewMs = 0) {
|
|
5186
|
+
if (record.expiresAt === void 0) return true;
|
|
5187
|
+
return record.expiresAt - (record.refreshToken ? refreshSkewMs : 0) > Date.now();
|
|
5188
|
+
}
|
|
5189
|
+
var HKDF_SALT = new TextEncoder().encode("frontmcp-token-store");
|
|
5190
|
+
function deriveKeyForRecord(masterKey, compositeKey, cache) {
|
|
5191
|
+
const cached = cache.get(compositeKey);
|
|
5192
|
+
if (cached) {
|
|
5193
|
+
return cached;
|
|
5194
|
+
}
|
|
5195
|
+
const info = new TextEncoder().encode(`orchestrated-token:${compositeKey}`);
|
|
5196
|
+
const derived = hkdfSha2563(masterKey, HKDF_SALT, info, 32);
|
|
5197
|
+
cache.set(compositeKey, derived);
|
|
5198
|
+
return derived;
|
|
5199
|
+
}
|
|
5200
|
+
function encryptRecord(key, record) {
|
|
5201
|
+
const plaintext = JSON.stringify(record);
|
|
5202
|
+
const iv = randomBytes7(12);
|
|
5203
|
+
const { ciphertext, tag } = encryptAesGcm4(key, new TextEncoder().encode(plaintext), iv);
|
|
5204
|
+
return JSON.stringify({
|
|
5205
|
+
iv: Buffer.from(iv).toString("base64url"),
|
|
5206
|
+
tag: Buffer.from(tag).toString("base64url"),
|
|
5207
|
+
data: Buffer.from(ciphertext).toString("base64url")
|
|
5208
|
+
});
|
|
5209
|
+
}
|
|
5210
|
+
function decryptRecord(key, encrypted) {
|
|
5211
|
+
const { iv, tag, data } = JSON.parse(encrypted);
|
|
5212
|
+
const ivBytes = Buffer.from(iv, "base64url");
|
|
5213
|
+
const tagBytes = Buffer.from(tag, "base64url");
|
|
5214
|
+
const ciphertextBytes = Buffer.from(data, "base64url");
|
|
5215
|
+
const plaintext = decryptAesGcm4(key, ciphertextBytes, ivBytes, tagBytes);
|
|
5216
|
+
return JSON.parse(new TextDecoder().decode(plaintext));
|
|
5217
|
+
}
|
|
5218
|
+
|
|
5219
|
+
// libs/auth/src/session/orchestrated-token.store.ts
|
|
4885
5220
|
var InMemoryOrchestratedTokenStore = class {
|
|
4886
5221
|
/** Token storage: Map<compositeKey, ProviderTokenRecord> */
|
|
4887
5222
|
tokens = /* @__PURE__ */ new Map();
|
|
@@ -4893,9 +5228,12 @@ var InMemoryOrchestratedTokenStore = class {
|
|
|
4893
5228
|
cleanupTimer;
|
|
4894
5229
|
/** Default TTL for records */
|
|
4895
5230
|
defaultTtlMs;
|
|
5231
|
+
/** How long before its expiry a refreshable access token stops being returned */
|
|
5232
|
+
refreshSkewMs;
|
|
4896
5233
|
constructor(options = {}) {
|
|
4897
5234
|
this.encryptionKey = options.encryptionKey;
|
|
4898
5235
|
this.defaultTtlMs = options.defaultTtlMs;
|
|
5236
|
+
this.refreshSkewMs = options.refreshSkewMs ?? 0;
|
|
4899
5237
|
const cleanupIntervalMs = options.cleanupIntervalMs ?? 6e4;
|
|
4900
5238
|
this.cleanupTimer = setInterval(() => {
|
|
4901
5239
|
void this.cleanup();
|
|
@@ -4923,7 +5261,7 @@ var InMemoryOrchestratedTokenStore = class {
|
|
|
4923
5261
|
}
|
|
4924
5262
|
const info = new TextEncoder().encode(`orchestrated-token:${compositeKey}`);
|
|
4925
5263
|
const salt = new TextEncoder().encode("frontmcp-token-store");
|
|
4926
|
-
const derivedKey =
|
|
5264
|
+
const derivedKey = hkdfSha2564(this.encryptionKey, salt, info, 32);
|
|
4927
5265
|
this.derivedKeys.set(compositeKey, derivedKey);
|
|
4928
5266
|
return derivedKey;
|
|
4929
5267
|
}
|
|
@@ -4933,8 +5271,8 @@ var InMemoryOrchestratedTokenStore = class {
|
|
|
4933
5271
|
async encryptRecord(compositeKey, record) {
|
|
4934
5272
|
const key = await this.deriveKeyForRecord(compositeKey);
|
|
4935
5273
|
const plaintext = JSON.stringify(record);
|
|
4936
|
-
const iv =
|
|
4937
|
-
const { ciphertext, tag } =
|
|
5274
|
+
const iv = randomBytes8(12);
|
|
5275
|
+
const { ciphertext, tag } = encryptAesGcm5(key, new TextEncoder().encode(plaintext), iv);
|
|
4938
5276
|
return JSON.stringify({
|
|
4939
5277
|
iv: Buffer.from(iv).toString("base64url"),
|
|
4940
5278
|
tag: Buffer.from(tag).toString("base64url"),
|
|
@@ -4950,7 +5288,7 @@ var InMemoryOrchestratedTokenStore = class {
|
|
|
4950
5288
|
const ivBytes = Buffer.from(iv, "base64url");
|
|
4951
5289
|
const tagBytes = Buffer.from(tag, "base64url");
|
|
4952
5290
|
const ciphertextBytes = Buffer.from(data, "base64url");
|
|
4953
|
-
const plaintext =
|
|
5291
|
+
const plaintext = decryptAesGcm5(key, ciphertextBytes, ivBytes, tagBytes);
|
|
4954
5292
|
return JSON.parse(new TextDecoder().decode(plaintext));
|
|
4955
5293
|
}
|
|
4956
5294
|
/**
|
|
@@ -4976,18 +5314,20 @@ var InMemoryOrchestratedTokenStore = class {
|
|
|
4976
5314
|
} else {
|
|
4977
5315
|
record = stored;
|
|
4978
5316
|
}
|
|
4979
|
-
|
|
5317
|
+
const dropAt = recordDropAt(record);
|
|
5318
|
+
if (dropAt && dropAt < Date.now()) {
|
|
4980
5319
|
this.tokens.delete(key);
|
|
4981
5320
|
return null;
|
|
4982
5321
|
}
|
|
4983
5322
|
return record;
|
|
4984
5323
|
}
|
|
4985
5324
|
/**
|
|
4986
|
-
* Retrieve decrypted access token for a provider
|
|
5325
|
+
* Retrieve decrypted access token for a provider, or null once it has expired (a record with a
|
|
5326
|
+
* refresh token keeps that, to renew it)
|
|
4987
5327
|
*/
|
|
4988
5328
|
async getAccessToken(authorizationId, providerId) {
|
|
4989
5329
|
const record = await this.getRecord(authorizationId, providerId);
|
|
4990
|
-
return record
|
|
5330
|
+
return record && hasLiveAccessToken(record, this.refreshSkewMs) ? record.accessToken : null;
|
|
4991
5331
|
}
|
|
4992
5332
|
/**
|
|
4993
5333
|
* Retrieve decrypted refresh token for a provider
|
|
@@ -5080,7 +5420,8 @@ var InMemoryOrchestratedTokenStore = class {
|
|
|
5080
5420
|
} else {
|
|
5081
5421
|
record = stored;
|
|
5082
5422
|
}
|
|
5083
|
-
|
|
5423
|
+
const dropAt = record ? recordDropAt(record) : void 0;
|
|
5424
|
+
if (dropAt && dropAt < now) {
|
|
5084
5425
|
keysToDelete.push(key);
|
|
5085
5426
|
}
|
|
5086
5427
|
}
|
|
@@ -5120,74 +5461,48 @@ var InMemoryOrchestratedTokenStore = class {
|
|
|
5120
5461
|
* @param toAuthId - Target authorization ID (e.g., "def456")
|
|
5121
5462
|
*/
|
|
5122
5463
|
async migrateTokens(fromAuthId, toAuthId) {
|
|
5123
|
-
const
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
if (key.startsWith(prefix)) {
|
|
5127
|
-
keysToMigrate.push(key);
|
|
5128
|
-
}
|
|
5464
|
+
for (const oldKey of await this.copyRecords(fromAuthId, toAuthId)) {
|
|
5465
|
+
this.tokens.delete(oldKey);
|
|
5466
|
+
this.derivedKeys.delete(oldKey);
|
|
5129
5467
|
}
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5468
|
+
}
|
|
5469
|
+
/**
|
|
5470
|
+
* Copy tokens from one authorization ID to another, keeping them under the source ID too.
|
|
5471
|
+
*/
|
|
5472
|
+
async copyTokens(fromAuthId, toAuthId) {
|
|
5473
|
+
await this.copyRecords(fromAuthId, toAuthId);
|
|
5474
|
+
}
|
|
5475
|
+
/** Copy every record of `fromAuthId` to `toAuthId`, skipping a corrupted one; returns the keys copied. */
|
|
5476
|
+
async copyRecords(fromAuthId, toAuthId) {
|
|
5477
|
+
const prefix = `${fromAuthId}:`;
|
|
5478
|
+
const sourceKeys = [...this.tokens.keys()].filter((key) => key.startsWith(prefix));
|
|
5479
|
+
const copiedKeys = [];
|
|
5480
|
+
for (const oldKey of sourceKeys) {
|
|
5133
5481
|
const stored = this.tokens.get(oldKey);
|
|
5134
|
-
if (!stored)
|
|
5135
|
-
|
|
5136
|
-
}
|
|
5482
|
+
if (!stored) continue;
|
|
5483
|
+
const newKey = this.buildKey(toAuthId, oldKey.slice(prefix.length));
|
|
5137
5484
|
if (this.encryptionKey) {
|
|
5138
5485
|
try {
|
|
5139
5486
|
const record = await this.decryptRecord(oldKey, stored);
|
|
5140
|
-
|
|
5141
|
-
this.tokens.set(newKey, encrypted);
|
|
5487
|
+
this.tokens.set(newKey, await this.encryptRecord(newKey, record));
|
|
5142
5488
|
} catch {
|
|
5143
5489
|
continue;
|
|
5144
5490
|
}
|
|
5145
5491
|
} else {
|
|
5146
5492
|
this.tokens.set(newKey, stored);
|
|
5147
5493
|
}
|
|
5148
|
-
|
|
5149
|
-
this.derivedKeys.delete(oldKey);
|
|
5494
|
+
copiedKeys.push(oldKey);
|
|
5150
5495
|
}
|
|
5496
|
+
return copiedKeys;
|
|
5151
5497
|
}
|
|
5152
5498
|
};
|
|
5153
5499
|
|
|
5154
|
-
// libs/auth/src/session/orchestrated-token.crypto.ts
|
|
5155
|
-
import { decryptAesGcm as decryptAesGcm5, encryptAesGcm as encryptAesGcm5, hkdfSha256 as hkdfSha2564, randomBytes as randomBytes8 } from "@frontmcp/utils";
|
|
5156
|
-
var HKDF_SALT = new TextEncoder().encode("frontmcp-token-store");
|
|
5157
|
-
function deriveKeyForRecord(masterKey, compositeKey, cache) {
|
|
5158
|
-
const cached = cache.get(compositeKey);
|
|
5159
|
-
if (cached) {
|
|
5160
|
-
return cached;
|
|
5161
|
-
}
|
|
5162
|
-
const info = new TextEncoder().encode(`orchestrated-token:${compositeKey}`);
|
|
5163
|
-
const derived = hkdfSha2564(masterKey, HKDF_SALT, info, 32);
|
|
5164
|
-
cache.set(compositeKey, derived);
|
|
5165
|
-
return derived;
|
|
5166
|
-
}
|
|
5167
|
-
function encryptRecord(key, record) {
|
|
5168
|
-
const plaintext = JSON.stringify(record);
|
|
5169
|
-
const iv = randomBytes8(12);
|
|
5170
|
-
const { ciphertext, tag } = encryptAesGcm5(key, new TextEncoder().encode(plaintext), iv);
|
|
5171
|
-
return JSON.stringify({
|
|
5172
|
-
iv: Buffer.from(iv).toString("base64url"),
|
|
5173
|
-
tag: Buffer.from(tag).toString("base64url"),
|
|
5174
|
-
data: Buffer.from(ciphertext).toString("base64url")
|
|
5175
|
-
});
|
|
5176
|
-
}
|
|
5177
|
-
function decryptRecord(key, encrypted) {
|
|
5178
|
-
const { iv, tag, data } = JSON.parse(encrypted);
|
|
5179
|
-
const ivBytes = Buffer.from(iv, "base64url");
|
|
5180
|
-
const tagBytes = Buffer.from(tag, "base64url");
|
|
5181
|
-
const ciphertextBytes = Buffer.from(data, "base64url");
|
|
5182
|
-
const plaintext = decryptAesGcm5(key, ciphertextBytes, ivBytes, tagBytes);
|
|
5183
|
-
return JSON.parse(new TextDecoder().decode(plaintext));
|
|
5184
|
-
}
|
|
5185
|
-
|
|
5186
5500
|
// libs/auth/src/session/storage-orchestrated-token.store.ts
|
|
5187
5501
|
var StorageOrchestratedTokenStore = class {
|
|
5188
5502
|
storage;
|
|
5189
5503
|
encryptionKey;
|
|
5190
5504
|
defaultTtlMs;
|
|
5505
|
+
refreshSkewMs;
|
|
5191
5506
|
namespace;
|
|
5192
5507
|
storageIsNamespaced;
|
|
5193
5508
|
/** HKDF-derived subkey cache, keyed by composite key. */
|
|
@@ -5195,6 +5510,7 @@ var StorageOrchestratedTokenStore = class {
|
|
|
5195
5510
|
constructor(storage, options = {}) {
|
|
5196
5511
|
this.encryptionKey = options.encryptionKey;
|
|
5197
5512
|
this.defaultTtlMs = options.defaultTtlMs;
|
|
5513
|
+
this.refreshSkewMs = options.refreshSkewMs ?? 0;
|
|
5198
5514
|
this.namespace = options.namespace ?? "otok";
|
|
5199
5515
|
this.storageIsNamespaced = this.isNamespacedStorage(storage);
|
|
5200
5516
|
this.storage = this.storageIsNamespaced ? storage.namespace(this.namespace) : storage;
|
|
@@ -5204,7 +5520,7 @@ var StorageOrchestratedTokenStore = class {
|
|
|
5204
5520
|
// ============================================
|
|
5205
5521
|
async getAccessToken(authorizationId, providerId) {
|
|
5206
5522
|
const record = await this.getRecord(authorizationId, providerId);
|
|
5207
|
-
return record
|
|
5523
|
+
return record && hasLiveAccessToken(record, this.refreshSkewMs) ? record.accessToken : null;
|
|
5208
5524
|
}
|
|
5209
5525
|
async getRefreshToken(authorizationId, providerId) {
|
|
5210
5526
|
const record = await this.getRecord(authorizationId, providerId);
|
|
@@ -5221,7 +5537,7 @@ var StorageOrchestratedTokenStore = class {
|
|
|
5221
5537
|
updatedAt: now
|
|
5222
5538
|
};
|
|
5223
5539
|
const value = this.encryptionKey ? encryptRecord(this.keyFor(compositeKey), record) : JSON.stringify(record);
|
|
5224
|
-
await this.storage.set(this.storageKey(compositeKey), value, this.ttlOptions(record
|
|
5540
|
+
await this.storage.set(this.storageKey(compositeKey), value, this.ttlOptions(recordDropAt(record)));
|
|
5225
5541
|
}
|
|
5226
5542
|
async deleteTokens(authorizationId, providerId) {
|
|
5227
5543
|
const compositeKey = this.compositeKey(authorizationId, providerId);
|
|
@@ -5247,12 +5563,37 @@ var StorageOrchestratedTokenStore = class {
|
|
|
5247
5563
|
return providerIds;
|
|
5248
5564
|
}
|
|
5249
5565
|
async migrateTokens(fromAuthId, toAuthId) {
|
|
5566
|
+
for (const oldComposite of await this.copyRecords(fromAuthId, toAuthId)) {
|
|
5567
|
+
await this.storage.delete(this.storageKey(oldComposite));
|
|
5568
|
+
this.derivedKeys.delete(oldComposite);
|
|
5569
|
+
}
|
|
5570
|
+
}
|
|
5571
|
+
async copyTokens(fromAuthId, toAuthId) {
|
|
5572
|
+
await this.copyRecords(fromAuthId, toAuthId);
|
|
5573
|
+
}
|
|
5574
|
+
/**
|
|
5575
|
+
* Delete all tokens for an authorization id.
|
|
5576
|
+
*/
|
|
5577
|
+
async deleteAllForAuthorization(authorizationId) {
|
|
5578
|
+
const prefix = `${authorizationId}:`;
|
|
5579
|
+
const compositeKeys = await this.listCompositeKeys(`${prefix}*`);
|
|
5580
|
+
for (const compositeKey of compositeKeys) {
|
|
5581
|
+
if (!compositeKey.startsWith(prefix)) continue;
|
|
5582
|
+
await this.storage.delete(this.storageKey(compositeKey));
|
|
5583
|
+
this.derivedKeys.delete(compositeKey);
|
|
5584
|
+
}
|
|
5585
|
+
}
|
|
5586
|
+
// ============================================
|
|
5587
|
+
// Internals
|
|
5588
|
+
// ============================================
|
|
5589
|
+
/** Copy every record of `fromAuthId` to `toAuthId`, skipping a corrupted one; returns the composite keys copied. */
|
|
5590
|
+
async copyRecords(fromAuthId, toAuthId) {
|
|
5250
5591
|
const prefix = `${fromAuthId}:`;
|
|
5251
5592
|
const compositeKeys = await this.listCompositeKeys(`${prefix}*`);
|
|
5593
|
+
const copiedKeys = [];
|
|
5252
5594
|
for (const oldComposite of compositeKeys) {
|
|
5253
5595
|
if (!oldComposite.startsWith(prefix)) continue;
|
|
5254
|
-
const
|
|
5255
|
-
const newComposite = this.compositeKey(toAuthId, providerId);
|
|
5596
|
+
const newComposite = this.compositeKey(toAuthId, oldComposite.slice(prefix.length));
|
|
5256
5597
|
const stored = await this.storage.get(this.storageKey(oldComposite));
|
|
5257
5598
|
if (stored === null) continue;
|
|
5258
5599
|
if (this.encryptionKey) {
|
|
@@ -5263,35 +5604,20 @@ var StorageOrchestratedTokenStore = class {
|
|
|
5263
5604
|
continue;
|
|
5264
5605
|
}
|
|
5265
5606
|
const value = encryptRecord(this.keyFor(newComposite), record);
|
|
5266
|
-
await this.storage.set(this.storageKey(newComposite), value, this.ttlOptions(record
|
|
5607
|
+
await this.storage.set(this.storageKey(newComposite), value, this.ttlOptions(recordDropAt(record)));
|
|
5267
5608
|
} else {
|
|
5268
|
-
let
|
|
5609
|
+
let dropAt;
|
|
5269
5610
|
try {
|
|
5270
|
-
|
|
5611
|
+
dropAt = recordDropAt(JSON.parse(stored));
|
|
5271
5612
|
} catch {
|
|
5272
|
-
|
|
5613
|
+
dropAt = void 0;
|
|
5273
5614
|
}
|
|
5274
|
-
await this.storage.set(this.storageKey(newComposite), stored, this.ttlOptions(
|
|
5615
|
+
await this.storage.set(this.storageKey(newComposite), stored, this.ttlOptions(dropAt));
|
|
5275
5616
|
}
|
|
5276
|
-
|
|
5277
|
-
this.derivedKeys.delete(oldComposite);
|
|
5278
|
-
}
|
|
5279
|
-
}
|
|
5280
|
-
/**
|
|
5281
|
-
* Delete all tokens for an authorization id.
|
|
5282
|
-
*/
|
|
5283
|
-
async deleteAllForAuthorization(authorizationId) {
|
|
5284
|
-
const prefix = `${authorizationId}:`;
|
|
5285
|
-
const compositeKeys = await this.listCompositeKeys(`${prefix}*`);
|
|
5286
|
-
for (const compositeKey of compositeKeys) {
|
|
5287
|
-
if (!compositeKey.startsWith(prefix)) continue;
|
|
5288
|
-
await this.storage.delete(this.storageKey(compositeKey));
|
|
5289
|
-
this.derivedKeys.delete(compositeKey);
|
|
5617
|
+
copiedKeys.push(oldComposite);
|
|
5290
5618
|
}
|
|
5619
|
+
return copiedKeys;
|
|
5291
5620
|
}
|
|
5292
|
-
// ============================================
|
|
5293
|
-
// Internals
|
|
5294
|
-
// ============================================
|
|
5295
5621
|
async getRecord(authorizationId, providerId) {
|
|
5296
5622
|
const compositeKey = this.compositeKey(authorizationId, providerId);
|
|
5297
5623
|
const stored = await this.storage.get(this.storageKey(compositeKey));
|
|
@@ -5314,7 +5640,8 @@ var StorageOrchestratedTokenStore = class {
|
|
|
5314
5640
|
return null;
|
|
5315
5641
|
}
|
|
5316
5642
|
}
|
|
5317
|
-
|
|
5643
|
+
const dropAt = recordDropAt(record);
|
|
5644
|
+
if (dropAt && dropAt < Date.now()) {
|
|
5318
5645
|
await this.storage.delete(this.storageKey(compositeKey));
|
|
5319
5646
|
return null;
|
|
5320
5647
|
}
|
|
@@ -6766,7 +7093,7 @@ var OrchestratedAuthorization = class _OrchestratedAuthorization extends Authori
|
|
|
6766
7093
|
const result = await this.#onTokenRefresh(providerId, refreshToken);
|
|
6767
7094
|
await this.#tokenStore.storeTokens(this.id, providerId, {
|
|
6768
7095
|
accessToken: result.accessToken,
|
|
6769
|
-
refreshToken: result.refreshToken,
|
|
7096
|
+
refreshToken: result.refreshToken ?? refreshToken,
|
|
6770
7097
|
expiresAt: result.expiresIn ? Date.now() + result.expiresIn * 1e3 : void 0
|
|
6771
7098
|
});
|
|
6772
7099
|
const currentState = this.#providerStates.get(providerId);
|
|
@@ -7535,7 +7862,7 @@ var CimdService = class {
|
|
|
7535
7862
|
cacheConfig;
|
|
7536
7863
|
securityConfig;
|
|
7537
7864
|
networkConfig;
|
|
7538
|
-
|
|
7865
|
+
cacheBackend;
|
|
7539
7866
|
logger;
|
|
7540
7867
|
/**
|
|
7541
7868
|
* Whether CIMD is enabled.
|
|
@@ -7555,7 +7882,6 @@ var CimdService = class {
|
|
|
7555
7882
|
this.cacheConfig = cimdCacheConfigSchema.parse(this.config.cache ?? {});
|
|
7556
7883
|
this.securityConfig = cimdSecurityConfigSchema.parse(this.config.security ?? {});
|
|
7557
7884
|
this.networkConfig = cimdNetworkConfigSchema.parse(this.config.network ?? {});
|
|
7558
|
-
this.cache = new InMemoryCimdCache(this.cacheConfig);
|
|
7559
7885
|
this.logger.debug("CimdService initialized", {
|
|
7560
7886
|
enabled: this.config.enabled,
|
|
7561
7887
|
cacheDefaultTtlMs: this.cacheConfig.defaultTtlMs,
|
|
@@ -7567,6 +7893,34 @@ var CimdService = class {
|
|
|
7567
7893
|
);
|
|
7568
7894
|
}
|
|
7569
7895
|
}
|
|
7896
|
+
/**
|
|
7897
|
+
* Create the cache now (connecting to Redis with `cache.type: 'redis'`), so a misconfigured or
|
|
7898
|
+
* unreachable cache fails at startup rather than on the first CIMD client.
|
|
7899
|
+
*/
|
|
7900
|
+
async initialize() {
|
|
7901
|
+
await this.cache();
|
|
7902
|
+
}
|
|
7903
|
+
/**
|
|
7904
|
+
* The cache `cache.type` selects, created once through the `createCimdCache` factory. A failed
|
|
7905
|
+
* creation is retried on the next use.
|
|
7906
|
+
*/
|
|
7907
|
+
cache() {
|
|
7908
|
+
if (!this.cacheBackend) {
|
|
7909
|
+
this.cacheBackend = createCimdCache(this.cacheConfig).catch((error) => {
|
|
7910
|
+
this.cacheBackend = void 0;
|
|
7911
|
+
throw error;
|
|
7912
|
+
});
|
|
7913
|
+
}
|
|
7914
|
+
return this.cacheBackend;
|
|
7915
|
+
}
|
|
7916
|
+
/** Close the cache's connection (Redis with `cache.type: 'redis'`), if one was created. */
|
|
7917
|
+
async dispose() {
|
|
7918
|
+
const backend = this.cacheBackend;
|
|
7919
|
+
this.cacheBackend = void 0;
|
|
7920
|
+
if (!backend) return;
|
|
7921
|
+
const createdBackend = await backend.catch(() => void 0);
|
|
7922
|
+
await createdBackend?.close?.();
|
|
7923
|
+
}
|
|
7570
7924
|
/**
|
|
7571
7925
|
* Check if a client_id is a CIMD URL.
|
|
7572
7926
|
*
|
|
@@ -7593,7 +7947,8 @@ var CimdService = class {
|
|
|
7593
7947
|
};
|
|
7594
7948
|
}
|
|
7595
7949
|
validateClientIdUrl(clientId, this.securityConfig);
|
|
7596
|
-
const
|
|
7950
|
+
const cache = await this.cache();
|
|
7951
|
+
const cached = await cache.get(clientId);
|
|
7597
7952
|
if (cached) {
|
|
7598
7953
|
this.logger.debug(`Cache hit for CIMD client: ${clientId}`);
|
|
7599
7954
|
return {
|
|
@@ -7611,8 +7966,8 @@ var CimdService = class {
|
|
|
7611
7966
|
if (this.securityConfig.warnOnLocalhostRedirects && hasOnlyLocalhostRedirectUris(document.redirect_uris)) {
|
|
7612
7967
|
this.logger.warn(`CIMD client "${clientId}" has only localhost redirect URIs - this may be a development client`);
|
|
7613
7968
|
}
|
|
7614
|
-
await
|
|
7615
|
-
const entry = await
|
|
7969
|
+
await cache.set(clientId, document, headers);
|
|
7970
|
+
const entry = await cache.get(clientId);
|
|
7616
7971
|
return {
|
|
7617
7972
|
isCimdClient: true,
|
|
7618
7973
|
metadata: document,
|
|
@@ -7642,11 +7997,12 @@ var CimdService = class {
|
|
|
7642
7997
|
* @param clientId - Optional client_id to clear; clears all if not provided
|
|
7643
7998
|
*/
|
|
7644
7999
|
async clearCache(clientId) {
|
|
8000
|
+
const cache = await this.cache();
|
|
7645
8001
|
if (clientId) {
|
|
7646
|
-
await
|
|
8002
|
+
await cache.delete(clientId);
|
|
7647
8003
|
this.logger.debug(`Cache cleared for: ${clientId}`);
|
|
7648
8004
|
} else {
|
|
7649
|
-
await
|
|
8005
|
+
await cache.clear();
|
|
7650
8006
|
this.logger.debug("Cache cleared");
|
|
7651
8007
|
}
|
|
7652
8008
|
}
|
|
@@ -7654,8 +8010,9 @@ var CimdService = class {
|
|
|
7654
8010
|
* Get cache statistics.
|
|
7655
8011
|
*/
|
|
7656
8012
|
async getCacheStats() {
|
|
8013
|
+
const cache = await this.cache();
|
|
7657
8014
|
return {
|
|
7658
|
-
size: await
|
|
8015
|
+
size: await cache.size()
|
|
7659
8016
|
};
|
|
7660
8017
|
}
|
|
7661
8018
|
/**
|
|
@@ -7667,7 +8024,8 @@ var CimdService = class {
|
|
|
7667
8024
|
const controller = new AbortController();
|
|
7668
8025
|
const timer = setTimeout(() => controller.abort(), this.networkConfig.timeoutMs);
|
|
7669
8026
|
try {
|
|
7670
|
-
const
|
|
8027
|
+
const cache = await this.cache();
|
|
8028
|
+
const conditionalHeaders = await cache.getConditionalHeaders(clientId);
|
|
7671
8029
|
const originalOrigin = new URL(clientId).origin;
|
|
7672
8030
|
const maxRedirects = this.networkConfig.maxRedirects;
|
|
7673
8031
|
let currentUrl = clientId;
|
|
@@ -7690,9 +8048,9 @@ var CimdService = class {
|
|
|
7690
8048
|
redirect: "manual"
|
|
7691
8049
|
});
|
|
7692
8050
|
if (response.status === 304) {
|
|
7693
|
-
const staleEntry = await
|
|
8051
|
+
const staleEntry = await cache.getStale(clientId);
|
|
7694
8052
|
if (staleEntry) {
|
|
7695
|
-
await
|
|
8053
|
+
await cache.revalidate(clientId, response.headers);
|
|
7696
8054
|
this.logger.debug(`CIMD document not modified: ${clientId}`);
|
|
7697
8055
|
return {
|
|
7698
8056
|
document: staleEntry.document,
|
|
@@ -8200,7 +8558,7 @@ var tokenRefreshConfigSchema = z10.object({
|
|
|
8200
8558
|
* Refresh token before expiry by this many seconds
|
|
8201
8559
|
* @default 60
|
|
8202
8560
|
*/
|
|
8203
|
-
skewSeconds: z10.number().default(60)
|
|
8561
|
+
skewSeconds: z10.number().nonnegative().default(60)
|
|
8204
8562
|
});
|
|
8205
8563
|
var skippedAppBehaviorSchema = z10.enum(["anonymous", "require-auth"]);
|
|
8206
8564
|
var consentConfigSchema = z10.object({
|
|
@@ -8313,7 +8671,7 @@ var publicAuthOptionsSchema = z11.object({
|
|
|
8313
8671
|
* Anonymous session TTL in seconds
|
|
8314
8672
|
* @default 3600 (1 hour)
|
|
8315
8673
|
*/
|
|
8316
|
-
sessionTtl: z11.number().default(3600),
|
|
8674
|
+
sessionTtl: z11.number().int().positive().default(3600),
|
|
8317
8675
|
/**
|
|
8318
8676
|
* Scopes granted to anonymous sessions
|
|
8319
8677
|
* @default ['anonymous']
|
|
@@ -8760,9 +9118,17 @@ function detectAuthProviders(parentAuth, apps) {
|
|
|
8760
9118
|
`Invalid auth configuration: Parent uses transparent mode but apps have their own auth providers. Transparent mode passes tokens through without modification, which is incompatible with multi-provider setups. Change parent auth to local or remote mode to properly manage tokens for each provider. Detected providers: ${[...providers.keys()].join(", ")}`
|
|
8761
9119
|
);
|
|
8762
9120
|
}
|
|
8763
|
-
|
|
8764
|
-
|
|
8765
|
-
|
|
9121
|
+
const sharedEndpointUnenforced = !parentAuth || isPublicMode(parentAuth) || isStaticMode(parentAuth);
|
|
9122
|
+
const unenforcedAppIds = apps.filter((app) => app.auth && !isPublicMode(app.auth) && !staticParentEnforces(parentAuth, app.auth)).map((app) => app.id);
|
|
9123
|
+
if (sharedEndpointUnenforced && unenforcedAppIds.length > 0) {
|
|
9124
|
+
validationErrors.push(
|
|
9125
|
+
`App-level auth is not enforced on the shared endpoint of a server in ${parentAuth?.mode ?? "public"} mode, so the tools of ${unenforcedAppIds.join(", ")} would be served without it. Serve the app on its own endpoint with standalone: true or splitByApp: true, or run the server in local or remote mode with incrementalAuth enabled, which checks each tool call against the apps the caller has authorized (without incrementalAuth, local and remote mode do not check app grants per tool call).`
|
|
9126
|
+
);
|
|
9127
|
+
}
|
|
9128
|
+
const tokenMintingAppIds = apps.filter((app) => app.remoteAuthMode === "forward" && !(parentAuth && isTransparentMode(parentAuth))).map((app) => app.id);
|
|
9129
|
+
if (tokenMintingAppIds.length > 0) {
|
|
9130
|
+
validationErrors.push(
|
|
9131
|
+
`remoteAuth: { mode: 'forward' } on ${tokenMintingAppIds.join(", ")} would send the remote a token this server minted or holds itself: only a server in transparent mode receives its callers' tokens from the identity provider. Use remoteAuth: { mode: 'static', credentials } (or transportOptions.headers) for the remote, or run the server in transparent mode.`
|
|
8766
9132
|
);
|
|
8767
9133
|
}
|
|
8768
9134
|
return {
|
|
@@ -8775,6 +9141,14 @@ function detectAuthProviders(parentAuth, apps) {
|
|
|
8775
9141
|
warnings
|
|
8776
9142
|
};
|
|
8777
9143
|
}
|
|
9144
|
+
function staticParentEnforces(parentAuth, appAuth) {
|
|
9145
|
+
if (!parentAuth || !isStaticMode(parentAuth) || !isStaticMode(appAuth)) return false;
|
|
9146
|
+
const parent = staticAuthOptionsSchema.safeParse(parentAuth);
|
|
9147
|
+
const app = staticAuthOptionsSchema.safeParse(appAuth);
|
|
9148
|
+
if (!parent.success || !app.success) return false;
|
|
9149
|
+
const sameChallenge = parent.data.header.toLowerCase() === app.data.header.toLowerCase() && parent.data.scheme === app.data.scheme;
|
|
9150
|
+
return sameChallenge && parent.data.tokens.every((token) => app.data.tokens.includes(token));
|
|
9151
|
+
}
|
|
8778
9152
|
function getProviderUrl(options) {
|
|
8779
9153
|
if (isTransparentMode(options)) {
|
|
8780
9154
|
return options.provider;
|
|
@@ -9115,6 +9489,16 @@ function isLoopbackRedirectUri(uri) {
|
|
|
9115
9489
|
return false;
|
|
9116
9490
|
}
|
|
9117
9491
|
|
|
9492
|
+
// libs/auth/src/utils/token-scopes.utils.ts
|
|
9493
|
+
function scopesFromClaims(claims) {
|
|
9494
|
+
const scopes = [claims?.scope, claims?.scp].flatMap((claim) => {
|
|
9495
|
+
if (typeof claim === "string") return claim.split(/\s+/).filter(Boolean);
|
|
9496
|
+
if (Array.isArray(claim)) return claim.filter((scope) => typeof scope === "string");
|
|
9497
|
+
return [];
|
|
9498
|
+
});
|
|
9499
|
+
return [...new Set(scopes)];
|
|
9500
|
+
}
|
|
9501
|
+
|
|
9118
9502
|
// libs/auth/src/vault/auth-providers.types.ts
|
|
9119
9503
|
import { z as z19 } from "@frontmcp/lazy-zod";
|
|
9120
9504
|
var credentialScopeSchema = z19.enum(["global", "user", "session"]);
|
|
@@ -10132,6 +10516,7 @@ var LazyCredentialLoader = class {
|
|
|
10132
10516
|
};
|
|
10133
10517
|
|
|
10134
10518
|
// libs/auth/src/authorities/authorities.context.ts
|
|
10519
|
+
import { getEnv as getEnv4 } from "@frontmcp/utils";
|
|
10135
10520
|
function resolveDotPath(obj, path) {
|
|
10136
10521
|
if (path in obj) {
|
|
10137
10522
|
return obj[path];
|
|
@@ -10172,6 +10557,10 @@ function signedInSubject(rawSub, mappedSub) {
|
|
|
10172
10557
|
if (rawSub !== void 0 && isAnonymousSubject(rawSub)) return void 0;
|
|
10173
10558
|
return [mappedSub, rawSub].find((candidate) => !isAnonymousSubject(candidate));
|
|
10174
10559
|
}
|
|
10560
|
+
var runtimeEnv = new Proxy(
|
|
10561
|
+
{},
|
|
10562
|
+
{ get: (_target, name) => typeof name === "string" ? getEnv4(name) : void 0 }
|
|
10563
|
+
);
|
|
10175
10564
|
var AuthoritiesContextBuilder = class {
|
|
10176
10565
|
claimsMapping;
|
|
10177
10566
|
claimsResolver;
|
|
@@ -10184,7 +10573,7 @@ var AuthoritiesContextBuilder = class {
|
|
|
10184
10573
|
/**
|
|
10185
10574
|
* Build an evaluation context from AuthInfo and tool input.
|
|
10186
10575
|
*/
|
|
10187
|
-
build(authInfo, input = {}, env =
|
|
10576
|
+
build(authInfo, input = {}, env = runtimeEnv) {
|
|
10188
10577
|
if (this.claimsResolver) {
|
|
10189
10578
|
const resolved = this.claimsResolver(authInfo);
|
|
10190
10579
|
return {
|
|
@@ -10254,9 +10643,9 @@ var FrontMcpAuthContextImpl = class {
|
|
|
10254
10643
|
claims;
|
|
10255
10644
|
roles;
|
|
10256
10645
|
permissions;
|
|
10257
|
-
constructor(source, claimsMapping) {
|
|
10646
|
+
constructor(source, claimsMapping, claimsResolver) {
|
|
10258
10647
|
const rawUser = resolveAuthUser(source);
|
|
10259
|
-
const mappedSub = claimsMapping?.userId ? resolveDotPath(this.buildRawClaims(source), claimsMapping.userId) : void 0;
|
|
10648
|
+
const mappedSub = !claimsResolver && claimsMapping?.userId ? resolveDotPath(this.buildRawClaims(source), claimsMapping.userId) : void 0;
|
|
10260
10649
|
const signedInSub = signedInSubject(rawUser.sub, mappedSub);
|
|
10261
10650
|
const sub = signedInSub ?? (typeof rawUser.sub === "string" ? rawUser.sub : "");
|
|
10262
10651
|
this.user = Object.freeze({
|
|
@@ -10274,6 +10663,13 @@ var FrontMcpAuthContextImpl = class {
|
|
|
10274
10663
|
this.scopes = Object.freeze(
|
|
10275
10664
|
Array.isArray(source.scopes) ? source.scopes.filter((s) => typeof s === "string") : []
|
|
10276
10665
|
);
|
|
10666
|
+
if (claimsResolver) {
|
|
10667
|
+
const resolved = claimsResolver(source);
|
|
10668
|
+
this.claims = Object.freeze({ ...resolved.claims });
|
|
10669
|
+
this.roles = Object.freeze([...toStringArray2(resolved.roles)]);
|
|
10670
|
+
this.permissions = Object.freeze([...toStringArray2(resolved.permissions)]);
|
|
10671
|
+
return;
|
|
10672
|
+
}
|
|
10277
10673
|
const rawClaims = this.buildRawClaims(source);
|
|
10278
10674
|
this.claims = Object.freeze({ ...rawClaims });
|
|
10279
10675
|
if (claimsMapping?.roles) {
|
|
@@ -10334,8 +10730,8 @@ var FrontMcpAuthContextImpl = class {
|
|
|
10334
10730
|
};
|
|
10335
10731
|
|
|
10336
10732
|
// libs/auth/src/context/frontmcp-auth-context.factory.ts
|
|
10337
|
-
function buildAuthContext(authInfo, claimsMapping, pipes) {
|
|
10338
|
-
const base = new FrontMcpAuthContextImpl(authInfo, claimsMapping);
|
|
10733
|
+
function buildAuthContext(authInfo, claimsMapping, pipes, claimsResolver) {
|
|
10734
|
+
const base = new FrontMcpAuthContextImpl(authInfo, claimsMapping, claimsResolver);
|
|
10339
10735
|
if (!pipes || pipes.length === 0) {
|
|
10340
10736
|
return base;
|
|
10341
10737
|
}
|
|
@@ -10587,6 +10983,10 @@ function evaluateCondition(condition, envelope, ctx) {
|
|
|
10587
10983
|
const expected = resolveValue(condition.value, ctx);
|
|
10588
10984
|
return applyOperator(actual, condition.op, expected);
|
|
10589
10985
|
}
|
|
10986
|
+
var REDACTED_ENV_VALUE = "[redacted]";
|
|
10987
|
+
function reportableActual(path, actual) {
|
|
10988
|
+
return path === "env" || path.startsWith("env.") ? REDACTED_ENV_VALUE : actual;
|
|
10989
|
+
}
|
|
10590
10990
|
function evaluateAbac(policy, ctx) {
|
|
10591
10991
|
const envelope = buildContextEnvelope(ctx);
|
|
10592
10992
|
if (policy.match) {
|
|
@@ -10594,10 +10994,11 @@ function evaluateAbac(policy, ctx) {
|
|
|
10594
10994
|
const actual = resolveDotPath(envelope, path);
|
|
10595
10995
|
const expected = resolveValue(expectedRaw, ctx);
|
|
10596
10996
|
if (expected === void 0 || actual !== expected) {
|
|
10997
|
+
const reported = reportableActual(path, actual);
|
|
10597
10998
|
return {
|
|
10598
10999
|
granted: false,
|
|
10599
|
-
deniedBy: `attributes.match: '${path}' expected '${String(expected)}' but got '${String(
|
|
10600
|
-
denial: { kind: "attributes", path: "attributes.match", expected, actual },
|
|
11000
|
+
deniedBy: `attributes.match: '${path}' expected '${String(expected)}' but got '${String(reported)}'`,
|
|
11001
|
+
denial: { kind: "attributes", path: "attributes.match", expected, actual: reported },
|
|
10601
11002
|
evaluatedPolicies: ["attributes.match"]
|
|
10602
11003
|
};
|
|
10603
11004
|
}
|
|
@@ -10608,7 +11009,7 @@ function evaluateAbac(policy, ctx) {
|
|
|
10608
11009
|
const condition = policy.conditions[i];
|
|
10609
11010
|
if (!evaluateCondition(condition, envelope, ctx)) {
|
|
10610
11011
|
const expected = resolveValue(condition.value, ctx);
|
|
10611
|
-
const actual = resolveDotPath(envelope, condition.path);
|
|
11012
|
+
const actual = reportableActual(condition.path, resolveDotPath(envelope, condition.path));
|
|
10612
11013
|
return {
|
|
10613
11014
|
granted: false,
|
|
10614
11015
|
deniedBy: `attributes.conditions: '${condition.path}' failed '${condition.op}' check against '${String(expected)}'`,
|
|
@@ -11347,6 +11748,21 @@ var basicApplier = {
|
|
|
11347
11748
|
function isFrontMcpCredentials(creds) {
|
|
11348
11749
|
return typeof creds === "object" && creds !== null && "provider" in creds && typeof creds.provider === "string" && creds.provider.length > 0;
|
|
11349
11750
|
}
|
|
11751
|
+
function providerHeadersCredentials(resolveHeaders) {
|
|
11752
|
+
return {
|
|
11753
|
+
async applyCredentials(_url, init) {
|
|
11754
|
+
const { credentials, ...cleanInit } = init;
|
|
11755
|
+
if (!isFrontMcpCredentials(credentials)) {
|
|
11756
|
+
return { init: typeof credentials === "string" ? init : cleanInit };
|
|
11757
|
+
}
|
|
11758
|
+
const headers = new Headers(cleanInit.headers);
|
|
11759
|
+
for (const [name, value] of Object.entries(await resolveHeaders(credentials.provider))) {
|
|
11760
|
+
headers.set(name, value);
|
|
11761
|
+
}
|
|
11762
|
+
return { init: { ...cleanInit, headers } };
|
|
11763
|
+
}
|
|
11764
|
+
};
|
|
11765
|
+
}
|
|
11350
11766
|
var FetchCredentialMiddleware = class {
|
|
11351
11767
|
constructor(tokenAccessor, appliers) {
|
|
11352
11768
|
this.tokenAccessor = tokenAccessor;
|
|
@@ -11647,6 +12063,7 @@ export {
|
|
|
11647
12063
|
privateKeyCredentialSchema,
|
|
11648
12064
|
progressiveAuthStateSchema,
|
|
11649
12065
|
providerConfigSchema,
|
|
12066
|
+
providerHeadersCredentials,
|
|
11650
12067
|
publicAccessConfigSchema,
|
|
11651
12068
|
publicAuthOptionsSchema,
|
|
11652
12069
|
queryApplier,
|
|
@@ -11664,6 +12081,7 @@ export {
|
|
|
11664
12081
|
resourceIdRefSchema,
|
|
11665
12082
|
resourceScopesFor,
|
|
11666
12083
|
safeDecrypt,
|
|
12084
|
+
scopesFromClaims,
|
|
11667
12085
|
sealPendingLogin,
|
|
11668
12086
|
secureStoreConfigSchema,
|
|
11669
12087
|
secureStoreCustomBackendSchema,
|