@frontmcp/auth 1.10.15 → 1.11.0-rc.1

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 (175) hide show
  1. package/browser/authorities/authorities.context.mjs +115 -0
  2. package/browser/authorities/authorities.engine.mjs +272 -0
  3. package/browser/authorities/authorities.errors.mjs +59 -0
  4. package/browser/authorities/authorities.evaluator.mjs +225 -0
  5. package/browser/authorities/authorities.metadata-augment.mjs +18 -0
  6. package/browser/authorities/authorities.registry.mjs +112 -0
  7. package/browser/authorities/authorities.schema.mjs +92 -0
  8. package/browser/authorities/authorities.scope-mapping.mjs +30 -0
  9. package/browser/authorities/authorities.validation.mjs +208 -0
  10. package/browser/authorities/index.mjs +43 -0
  11. package/browser/authorization/authorization.class.mjs +252 -0
  12. package/browser/authorization/authorization.types.mjs +64 -0
  13. package/browser/authorization/dcr-client.registry.mjs +222 -0
  14. package/browser/authorization/index.mjs +41 -0
  15. package/browser/authorization/orchestrated.accessor.mjs +86 -0
  16. package/browser/authorization/orchestrated.authorization.mjs +289 -0
  17. package/browser/authorization/public.authorization.mjs +124 -0
  18. package/browser/authorization/transparent.authorization.mjs +130 -0
  19. package/browser/cimd/cimd-redis.cache.mjs +231 -0
  20. package/browser/cimd/cimd.cache.mjs +215 -0
  21. package/browser/cimd/cimd.errors.mjs +142 -0
  22. package/browser/cimd/cimd.logger.mjs +15 -0
  23. package/browser/cimd/cimd.service.mjs +376 -0
  24. package/browser/cimd/cimd.types.mjs +228 -0
  25. package/browser/cimd/cimd.validator.mjs +217 -0
  26. package/browser/cimd/index.mjs +63 -0
  27. package/browser/common/auth-logger.interface.mjs +14 -0
  28. package/browser/common/index.mjs +6 -0
  29. package/browser/common/jwt.types.mjs +38 -0
  30. package/browser/common/session-user.types.mjs +0 -0
  31. package/browser/common/session.types.mjs +58 -0
  32. package/browser/common/zod-utils.mjs +0 -0
  33. package/browser/consent/consent.types.mjs +94 -0
  34. package/browser/consent/index.mjs +17 -0
  35. package/browser/context/frontmcp-auth-context.factory.mjs +29 -0
  36. package/browser/context/frontmcp-auth-context.impl.mjs +112 -0
  37. package/browser/context/index.mjs +7 -0
  38. package/browser/detection/auth-provider-detection.mjs +176 -0
  39. package/browser/detection/index.mjs +19 -0
  40. package/browser/errors/auth-internal.error.mjs +42 -0
  41. package/browser/errors/auth-internal.errors.mjs +150 -0
  42. package/browser/errors/index.mjs +53 -0
  43. package/browser/fetch/fetch-credential-middleware.mjs +91 -0
  44. package/browser/fetch/index.mjs +19 -0
  45. package/browser/index.mjs +573 -0
  46. package/browser/jwks/index.mjs +9 -0
  47. package/browser/jwks/jose.loader.mjs +28 -0
  48. package/browser/jwks/jwks.service.mjs +521 -0
  49. package/browser/jwks/jwks.utils.mjs +33 -0
  50. package/browser/options/app-auth.schema.mjs +18 -0
  51. package/browser/options/index.mjs +89 -0
  52. package/browser/options/orchestrated.schema.mjs +128 -0
  53. package/browser/options/public.schema.mjs +39 -0
  54. package/browser/options/schema.mjs +16 -0
  55. package/browser/options/shared.schemas.mjs +506 -0
  56. package/browser/options/static.schema.mjs +41 -0
  57. package/browser/options/transparent.schema.mjs +43 -0
  58. package/browser/options/utils.mjs +47 -0
  59. package/browser/people/index.mjs +15 -0
  60. package/browser/people/loopback-token.auth.mjs +115 -0
  61. package/browser/people/people-auth.options.mjs +37 -0
  62. package/browser/session/authorization-vault.mjs +279 -0
  63. package/browser/session/authorization.store.mjs +336 -0
  64. package/browser/session/consent.store.mjs +34 -0
  65. package/browser/session/credential-resume-link.mjs +43 -0
  66. package/browser/session/credentials-accessor.impl.mjs +54 -0
  67. package/browser/session/credentials-accessor.mjs +5 -0
  68. package/browser/session/encrypted-authorization-vault.mjs +405 -0
  69. package/browser/session/federated-auth.session.mjs +176 -0
  70. package/browser/session/incremental-auth-ticket.mjs +42 -0
  71. package/browser/session/index.mjs +266 -0
  72. package/browser/session/orchestrated-token.crypto.mjs +47 -0
  73. package/browser/session/orchestrated-token.store.mjs +286 -0
  74. package/browser/session/pending-login.mjs +37 -0
  75. package/browser/session/redis-session.store.mjs +255 -0
  76. package/browser/session/secure-store-accessor.impl.mjs +78 -0
  77. package/browser/session/secure-store-accessor.mjs +5 -0
  78. package/browser/session/secure-store-backends.mjs +111 -0
  79. package/browser/session/secure-store.factory.mjs +77 -0
  80. package/browser/session/session-credential-vault.factory.mjs +18 -0
  81. package/browser/session/session-credential-vault.mjs +243 -0
  82. package/browser/session/session-crypto.mjs +39 -0
  83. package/browser/session/session-rate-limiter.mjs +126 -0
  84. package/browser/session/session.transport.mjs +16 -0
  85. package/browser/session/signin-binding.mjs +30 -0
  86. package/browser/session/storage/in-memory-authorization-vault.mjs +28 -0
  87. package/browser/session/storage/index.mjs +14 -0
  88. package/browser/session/storage/storage-authorization-vault.mjs +301 -0
  89. package/browser/session/storage/storage-token-store.mjs +83 -0
  90. package/browser/session/storage-authorization.store.mjs +134 -0
  91. package/browser/session/storage-consent.store.mjs +33 -0
  92. package/browser/session/storage-federated-auth.session.mjs +55 -0
  93. package/browser/session/storage-orchestrated-token.store.mjs +197 -0
  94. package/browser/session/token-storage.factory.mjs +58 -0
  95. package/browser/session/token.refresh.mjs +36 -0
  96. package/browser/session/token.vault.mjs +61 -0
  97. package/browser/session/transport-session.types.mjs +108 -0
  98. package/browser/session/utils/auth-token.utils.mjs +58 -0
  99. package/browser/session/utils/index.mjs +23 -0
  100. package/browser/session/utils/session-crypto.utils.mjs +49 -0
  101. package/browser/session/utils/tiny-ttl-cache.mjs +46 -0
  102. package/browser/session/vault-encryption.mjs +187 -0
  103. package/browser/session/vercel-kv-session.store.mjs +230 -0
  104. package/browser/shared-server/index.mjs +17 -0
  105. package/browser/shared-server/shared-server-identity.mjs +55 -0
  106. package/browser/ui/base-layout.mjs +184 -0
  107. package/browser/ui/index.mjs +51 -0
  108. package/browser/ui/local-login.helper.mjs +46 -0
  109. package/browser/ui/templates.mjs +482 -0
  110. package/browser/utils/audience.validator.mjs +110 -0
  111. package/browser/utils/authorization-id.utils.mjs +10 -0
  112. package/browser/utils/index.mjs +45 -0
  113. package/browser/utils/redirect-uri.utils.mjs +22 -0
  114. package/browser/utils/scope-grant.utils.mjs +36 -0
  115. package/browser/utils/token-scopes.utils.mjs +12 -0
  116. package/browser/utils/www-authenticate.utils.mjs +164 -0
  117. package/browser/vault/auth-providers.accessor.impl.mjs +236 -0
  118. package/browser/vault/auth-providers.accessor.mjs +5 -0
  119. package/browser/vault/auth-providers.registry.mjs +140 -0
  120. package/browser/vault/auth-providers.types.mjs +47 -0
  121. package/browser/vault/auth-providers.vault.mjs +206 -0
  122. package/browser/vault/credential-cache.mjs +187 -0
  123. package/browser/vault/credential-helpers.mjs +15 -0
  124. package/browser/vault/credential-loaders/eager-loader.mjs +91 -0
  125. package/browser/vault/credential-loaders/lazy-loader.mjs +120 -0
  126. package/browser/vault/index.mjs +35 -0
  127. package/cimd/cimd.types.d.ts +1 -1
  128. package/cimd/cimd.types.d.ts.map +1 -1
  129. package/common/jwt.types.d.ts +1 -1
  130. package/common/jwt.types.d.ts.map +1 -1
  131. package/common/session.types.d.ts +9 -1
  132. package/common/session.types.d.ts.map +1 -1
  133. package/consent/consent.types.d.ts +1 -1
  134. package/consent/consent.types.d.ts.map +1 -1
  135. package/detection/auth-provider-detection.d.ts +1 -1
  136. package/detection/auth-provider-detection.d.ts.map +1 -1
  137. package/errors/auth-internal.errors.d.ts +6 -0
  138. package/errors/auth-internal.errors.d.ts.map +1 -1
  139. package/errors/index.d.ts +1 -1
  140. package/errors/index.d.ts.map +1 -1
  141. package/esm/index.mjs +2 -2
  142. package/esm/package.json +21 -5
  143. package/index.d.ts +5 -1
  144. package/index.d.ts.map +1 -1
  145. package/index.js +2 -2
  146. package/options/app-auth.schema.d.ts +19 -1
  147. package/options/app-auth.schema.d.ts.map +1 -1
  148. package/options/orchestrated.schema.d.ts +37 -1
  149. package/options/orchestrated.schema.d.ts.map +1 -1
  150. package/options/public.schema.d.ts +1 -1
  151. package/options/public.schema.d.ts.map +1 -1
  152. package/options/schema.d.ts +19 -1
  153. package/options/schema.d.ts.map +1 -1
  154. package/options/shared.schemas.d.ts +10 -14
  155. package/options/shared.schemas.d.ts.map +1 -1
  156. package/options/static.schema.d.ts +1 -1
  157. package/options/static.schema.d.ts.map +1 -1
  158. package/options/transparent.schema.d.ts +1 -1
  159. package/options/transparent.schema.d.ts.map +1 -1
  160. package/package.json +21 -5
  161. package/people/index.d.ts +3 -0
  162. package/people/index.d.ts.map +1 -0
  163. package/people/loopback-token.auth.d.ts +75 -0
  164. package/people/loopback-token.auth.d.ts.map +1 -0
  165. package/people/people-auth.options.d.ts +36 -0
  166. package/people/people-auth.options.d.ts.map +1 -0
  167. package/session/authorization-vault.d.ts +1 -1
  168. package/session/authorization-vault.d.ts.map +1 -1
  169. package/session/encrypted-authorization-vault.d.ts +1 -1
  170. package/session/encrypted-authorization-vault.d.ts.map +1 -1
  171. package/session/secure-store.factory.d.ts.map +1 -1
  172. package/shared-server/index.d.ts +3 -0
  173. package/shared-server/index.d.ts.map +1 -0
  174. package/shared-server/shared-server-identity.d.ts +26 -0
  175. package/shared-server/shared-server-identity.d.ts.map +1 -0
@@ -0,0 +1,521 @@
1
+ var __frontmcpModule = typeof process !== "undefined" && typeof process.getBuiltinModule === "function" ? process.getBuiltinModule("module") : undefined;
2
+ const require = __frontmcpModule ? __frontmcpModule.createRequire(import.meta.url || 'file:///') : undefined;
3
+ // __frontmcpCreateRequire
4
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
5
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
6
+ }) : x)(function(x) {
7
+ if (typeof require !== "undefined") return require.apply(this, arguments);
8
+ throw Error('Dynamic require of "' + x + '" is not supported');
9
+ });
10
+
11
+ // libs/auth/src/jwks/jwks.service.ts
12
+ import {
13
+ bytesToHex,
14
+ createKeyPersistence,
15
+ isProduction,
16
+ isRedirectResponse,
17
+ randomBytes,
18
+ rsaVerify
19
+ } from "@frontmcp/utils";
20
+ import { resolveAndCheckHostname } from "../cimd/cimd.validator.mjs";
21
+ import { noopAuthLogger } from "../common/auth-logger.interface.mjs";
22
+ import { loadJose } from "./jose.loader.mjs";
23
+ import { normalizeIssuer, trimSlash } from "./jwks.utils.mjs";
24
+ var TRANSPARENT_JWT_ALGS = [
25
+ "RS256",
26
+ "RS384",
27
+ "RS512",
28
+ "PS256",
29
+ "PS384",
30
+ "PS512",
31
+ "ES256",
32
+ "ES384",
33
+ "ES512",
34
+ "EdDSA"
35
+ ];
36
+ var MAX_JWKS_FETCH_BYTES = 1048576;
37
+ var MAX_JWKS_FETCH_HOPS = 3;
38
+ var UNKNOWN_KID_REFETCH_COOLDOWN_MS = 6e4;
39
+ var MISSING_EXP_ERROR = 'missing required "exp" claim';
40
+ function isMissingExpError(error) {
41
+ if (typeof error !== "object" || error === null) return false;
42
+ const { code, claim, reason } = error;
43
+ return code === "ERR_JWT_CLAIM_VALIDATION_FAILED" && claim === "exp" && reason === "missing";
44
+ }
45
+ var WEAK_KEY_WARNING = `
46
+ \u26A0\uFE0F SECURITY WARNING: OAuth provider is using an RSA key smaller than 2048 bits.
47
+ This is considered insecure and should be updated.
48
+ Please contact your OAuth provider to upgrade their signing keys.
49
+ Verification will proceed but with reduced security guarantees.
50
+ `;
51
+ var JwksService = class {
52
+ opts;
53
+ logger;
54
+ warnedProviders = /* @__PURE__ */ new Set();
55
+ // Orchestrator signing material
56
+ orchestratorKey;
57
+ // Provider JWKS cache (providerId -> jwks + fetchedAt)
58
+ providerJwks = /* @__PURE__ */ new Map();
59
+ // The last key refetch an unknown `kid` started, per provider (see refetchForUnknownKid)
60
+ unknownKidRefetches = /* @__PURE__ */ new Map();
61
+ // Track if key has been initialized (for async loading)
62
+ keyInitialized = false;
63
+ // Promise guard to prevent concurrent key generation
64
+ keyInitPromise;
65
+ // KeyPersistence instance for unified key storage
66
+ keyPersistence;
67
+ constructor(opts) {
68
+ this.logger = opts?.logger ?? noopAuthLogger;
69
+ this.opts = {
70
+ orchestratorAlg: opts?.orchestratorAlg ?? "RS256",
71
+ rotateDays: opts?.rotateDays ?? 30,
72
+ providerJwksTtlMs: opts?.providerJwksTtlMs ?? 6 * 60 * 60 * 1e3,
73
+ // 6h
74
+ networkTimeoutMs: opts?.networkTimeoutMs ?? 5e3
75
+ // 5s
76
+ };
77
+ }
78
+ // ===========================================================================
79
+ // Key Persistence Helpers
80
+ // ===========================================================================
81
+ /**
82
+ * Get or create the KeyPersistence instance.
83
+ * Returns null if persistence is disabled (production).
84
+ */
85
+ async getKeyPersistence() {
86
+ if (isProduction()) return null;
87
+ if (!this.keyPersistence) {
88
+ this.keyPersistence = await createKeyPersistence({
89
+ baseDir: ".frontmcp/keys"
90
+ });
91
+ }
92
+ return this.keyPersistence;
93
+ }
94
+ // ===========================================================================
95
+ // Public JWKS (what /.well-known/jwks.json serves)
96
+ // ===========================================================================
97
+ /** Gateway's public JWKS (publish at /.well-known/jwks.json when orchestrated). */
98
+ async getPublicJwks() {
99
+ return this.getOrchestratorJwks();
100
+ }
101
+ // ===========================================================================
102
+ // Scope-aware verification API
103
+ // ===========================================================================
104
+ /**
105
+ * Verify a token against candidate transparent providers.
106
+ * Ensures JWKS are available (cached/TTL/AS discovery) per provider.
107
+ */
108
+ async verifyTransparentToken(token, candidates) {
109
+ if (!candidates?.length) return { ok: false, error: "no_providers" };
110
+ const { createLocalJWKSet, decodeProtectedHeader, jwtVerify } = await loadJose();
111
+ let kid;
112
+ try {
113
+ const header = decodeProtectedHeader(token);
114
+ kid = typeof header?.kid === "string" ? header.kid : void 0;
115
+ } catch {
116
+ }
117
+ let missingExp = false;
118
+ for (const p of candidates) {
119
+ let jwks;
120
+ try {
121
+ jwks = await this.getJwksForProvider(p);
122
+ if (kid && !p.jwks?.keys?.length && !jwks?.keys?.some((key) => key.kid === kid)) {
123
+ jwks = await this.refetchForUnknownKid(p) ?? jwks;
124
+ }
125
+ if (!jwks?.keys?.length) continue;
126
+ const JWKS = createLocalJWKSet(jwks);
127
+ const issuerConstraint = this.issuerConstraintFor(p);
128
+ const { payload, protectedHeader } = await jwtVerify(token, JWKS, {
129
+ // Pin to asymmetric algorithms so a symmetric key in the provider JWKS
130
+ // can't be used to accept an HMAC-signed token, and `none` is refused.
131
+ algorithms: [...TRANSPARENT_JWT_ALGS],
132
+ ...issuerConstraint ? { issuer: issuerConstraint } : {},
133
+ // A bearer token without `exp` would be valid forever (#272).
134
+ requiredClaims: ["exp"]
135
+ });
136
+ return {
137
+ ok: true,
138
+ issuer: payload?.iss,
139
+ sub: payload?.sub,
140
+ providerId: p.id,
141
+ header: protectedHeader,
142
+ payload
143
+ };
144
+ } catch (e) {
145
+ if (this.isWeakKeyError(e)) {
146
+ const fallbackJwks = jwks ?? await this.getJwksForProvider(p);
147
+ if (fallbackJwks?.keys?.length) {
148
+ const fallbackResult = await this.verifyWithWeakKey(token, fallbackJwks, p);
149
+ if (fallbackResult.ok) {
150
+ return fallbackResult;
151
+ }
152
+ if (fallbackResult.error === "missing_exp") missingExp = true;
153
+ }
154
+ }
155
+ if (isMissingExpError(e)) missingExp = true;
156
+ this.logger.debug("[JwksService] Failed to verify token for provider: %s", p.id, e);
157
+ }
158
+ }
159
+ if (missingExp) return { ok: false, error: MISSING_EXP_ERROR };
160
+ return { ok: false, error: `no_provider_verified${kid ? ` (kid=${kid})` : ""}` };
161
+ }
162
+ /**
163
+ * Issuer constraint passed to `jose` for a provider: the configured issuer
164
+ * URL plus any explicitly configured `additionalIssuers`, each accepted with
165
+ * and without a trailing slash (`jose` compares `iss` byte-for-byte and IdPs
166
+ * differ on the trailing slash).
167
+ *
168
+ * Returns `undefined` ONLY when the provider explicitly opts out via
169
+ * `verifyIssuer: false`, which disables issuer checking entirely. Otherwise
170
+ * the set is derived ONLY from configuration \u2014 the token's own `iss` claim
171
+ * must never be appended, since an allowlist that contains the unverified
172
+ * claim accepts every issuer (GHSA-hvvp-67p3-j379). An empty/whitespace
173
+ * `issuerUrl` yields a set that no real token can satisfy (safe fail), never
174
+ * a silent bypass.
175
+ */
176
+ issuerConstraintFor(p) {
177
+ if (p.verifyIssuer === false) return void 0;
178
+ const issuers = /* @__PURE__ */ new Set();
179
+ for (const candidate of [p.issuerUrl, ...p.additionalIssuers ?? []]) {
180
+ const normalized = normalizeIssuer(candidate);
181
+ if (!normalized) continue;
182
+ issuers.add(normalized);
183
+ issuers.add(`${normalized}/`);
184
+ }
185
+ return [...issuers];
186
+ }
187
+ /**
188
+ * Check if the error is due to weak RSA key (< 2048 bits)
189
+ */
190
+ isWeakKeyError(error) {
191
+ const message = typeof error === "object" && error !== null && "message" in error && typeof error.message === "string" ? error.message : String(error);
192
+ return message.includes("modulusLength") && message.includes("2048");
193
+ }
194
+ /**
195
+ * Fallback verification for providers using RSA keys smaller than 2048 bits.
196
+ * Logs a security warning but allows verification to proceed.
197
+ */
198
+ async verifyWithWeakKey(token, jwks, provider) {
199
+ try {
200
+ const parts = token.split(".");
201
+ if (parts.length !== 3) {
202
+ return { ok: false, error: "invalid_token_format" };
203
+ }
204
+ const [headerB64, payloadB64, signatureB64] = parts;
205
+ const header = JSON.parse(Buffer.from(headerB64, "base64url").toString("utf8"));
206
+ const payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString("utf8"));
207
+ const matchingKey = this.findMatchingKey(jwks, header);
208
+ if (!matchingKey) {
209
+ return { ok: false, error: "no_matching_key" };
210
+ }
211
+ const signatureInput = `${headerB64}.${payloadB64}`;
212
+ const signature = Buffer.from(signatureB64, "base64url");
213
+ const jwtAlg = typeof header.alg === "string" ? header.alg : void 0;
214
+ if (!jwtAlg) {
215
+ return { ok: false, error: "missing_alg" };
216
+ }
217
+ const isValid = await rsaVerify(
218
+ jwtAlg,
219
+ Buffer.from(signatureInput),
220
+ matchingKey,
221
+ signature
222
+ );
223
+ if (!isValid) {
224
+ return { ok: false, error: "signature_invalid" };
225
+ }
226
+ if (provider.verifyIssuer !== false) {
227
+ const payloadIssuerRaw = typeof payload.iss === "string" ? payload.iss : void 0;
228
+ if (!payloadIssuerRaw) {
229
+ return { ok: false, error: "issuer_mismatch" };
230
+ }
231
+ const trustedIssuers = new Set(
232
+ [provider.issuerUrl, ...provider.additionalIssuers ?? []].map((issuer) => normalizeIssuer(issuer))
233
+ );
234
+ const payloadIssuer = normalizeIssuer(payloadIssuerRaw);
235
+ if (!trustedIssuers.has(payloadIssuer)) {
236
+ return { ok: false, error: "issuer_mismatch" };
237
+ }
238
+ }
239
+ const nowSec = Math.floor(Date.now() / 1e3);
240
+ if (payload.exp === void 0) {
241
+ return { ok: false, error: "missing_exp" };
242
+ }
243
+ if (typeof payload.exp !== "number" || !Number.isFinite(payload.exp) || payload.exp <= nowSec) {
244
+ return { ok: false, error: "token_expired" };
245
+ }
246
+ if (payload.nbf !== void 0 && (typeof payload.nbf !== "number" || !Number.isFinite(payload.nbf) || payload.nbf > nowSec)) {
247
+ return { ok: false, error: "token_not_yet_valid" };
248
+ }
249
+ if (!this.warnedProviders.has(provider.id)) {
250
+ this.warnedProviders.add(provider.id);
251
+ this.logger.warn(WEAK_KEY_WARNING);
252
+ this.logger.warn(" Provider: %s (%s)", provider.id, provider.issuerUrl);
253
+ }
254
+ return {
255
+ ok: true,
256
+ issuer: payload.iss,
257
+ sub: payload.sub,
258
+ providerId: provider.id,
259
+ header,
260
+ payload
261
+ };
262
+ } catch (err) {
263
+ const message = err instanceof Error ? err.message : "unknown";
264
+ return { ok: false, error: `weak_key_verification_failed: ${message}` };
265
+ }
266
+ }
267
+ /**
268
+ * Find a matching key from JWKS based on token header
269
+ */
270
+ findMatchingKey(jwks, header) {
271
+ if (header.kid) {
272
+ return jwks.keys.find((k) => k.kid === header.kid);
273
+ }
274
+ if (header.alg) {
275
+ const byAlg = jwks.keys.find((k) => k.alg === header.alg || k.kty === "RSA" && header.alg?.startsWith("RS"));
276
+ if (byAlg) return byAlg;
277
+ }
278
+ return jwks.keys.find((k) => k.kty === "RSA");
279
+ }
280
+ // ===========================================================================
281
+ // Provider JWKS (cache + preload + discovery)
282
+ // ===========================================================================
283
+ /** Directly set provider JWKS (e.g., inline keys from config). */
284
+ setProviderJwks(providerId, jwks) {
285
+ this.providerJwks.set(providerId, { jwks, fetchedAt: Date.now() });
286
+ }
287
+ /**
288
+ * Ensure JWKS for a provider:
289
+ * 1) inline jwks (if provided) \u2192 cache & return
290
+ * 2) cached & fresh (TTL) \u2192 return
291
+ * 3) explicit jwksUri \u2192 fetch, cache, return
292
+ * 4) `<issuer>/.well-known/jwks.json` \u2192 fetch, cache, return
293
+ * 5) discover jwks_uri \u2192 the issuer's OAuth authorization server
294
+ * metadata (RFC 8414), else its OpenID configuration (OpenID Connect
295
+ * Discovery 1.0 \u00a74, where Google, Okta, Auth0 and Entra publish it),
296
+ * then fetch jwks_uri, cache, return
297
+ */
298
+ async getJwksForProvider(ref) {
299
+ if (ref.jwks?.keys?.length) {
300
+ this.setProviderJwks(ref.id, ref.jwks);
301
+ return ref.jwks;
302
+ }
303
+ const cached = this.providerJwks.get(ref.id);
304
+ if (cached && Date.now() - cached.fetchedAt < this.opts.providerJwksTtlMs) {
305
+ return cached.jwks;
306
+ }
307
+ const fetched = await this.fetchProviderJwks(ref);
308
+ if (fetched) return fetched;
309
+ const maxStaleMs = Math.max(this.opts.providerJwksTtlMs * 4, 6e4);
310
+ if (cached && Date.now() - cached.fetchedAt < maxStaleMs) {
311
+ return cached.jwks;
312
+ }
313
+ return void 0;
314
+ }
315
+ /**
316
+ * Fetch a provider's keys again for a token whose `kid` the cached keys don't have (a key the
317
+ * provider started using since), ahead of the cache's TTL. At most one refetch per provider per
318
+ * {@link UNKNOWN_KID_REFETCH_COOLDOWN_MS}: tokens that arrive meanwhile wait for the one in flight or
319
+ * use its result, so a flood of tokens with made-up `kid`s can't hammer the provider. Only freshly
320
+ * fetched keys are returned: a failed refetch never revives cached keys past the stale window.
321
+ */
322
+ refetchForUnknownKid(ref) {
323
+ let refetch = this.unknownKidRefetches.get(ref.id);
324
+ if (!refetch || Date.now() - refetch.startedAt >= UNKNOWN_KID_REFETCH_COOLDOWN_MS) {
325
+ refetch = { startedAt: Date.now(), done: this.fetchProviderJwks(ref) };
326
+ this.unknownKidRefetches.set(ref.id, refetch);
327
+ }
328
+ return refetch.done;
329
+ }
330
+ /**
331
+ * Fetch a provider's keys, and cache them: from `jwksUri`, else the conventional location, else
332
+ * the `jwks_uri` of the issuer's OAuth or OpenID metadata. Every URL goes through the same scheme
333
+ * and SSRF checks (`fetchJson`), `jwks_uri` included.
334
+ */
335
+ async fetchProviderJwks(ref) {
336
+ if (ref.jwksUri) {
337
+ const fromUri = await this.tryFetchJwks(ref.id, ref.jwksUri);
338
+ if (fromUri?.keys?.length) return fromUri;
339
+ }
340
+ const issuer = trimSlash(ref.issuerUrl);
341
+ const fromWellKnown = await this.tryFetchJwks(ref.id, `${issuer}/.well-known/jwks.json`);
342
+ if (fromWellKnown?.keys?.length) return fromWellKnown;
343
+ for (const document of ["oauth-authorization-server", "openid-configuration"]) {
344
+ const meta = await this.tryFetchAsMeta(`${issuer}/.well-known/${document}`);
345
+ const uri = meta && typeof meta === "object" && meta["jwks_uri"] ? String(meta["jwks_uri"]) : void 0;
346
+ if (uri) {
347
+ const fromMeta = await this.tryFetchJwks(ref.id, uri);
348
+ if (fromMeta?.keys?.length) return fromMeta;
349
+ }
350
+ }
351
+ return void 0;
352
+ }
353
+ // ===========================================================================
354
+ // Orchestrator keys (generation/rotation)
355
+ // ===========================================================================
356
+ /** Return the orchestrator public JWKS (generates/rotates as needed). */
357
+ async getOrchestratorJwks() {
358
+ await this.ensureOrchestratorKey();
359
+ return this.orchestratorKey.publicJwk;
360
+ }
361
+ /** Return private signing key + kid for issuing orchestrator tokens. */
362
+ async getOrchestratorSigningKey() {
363
+ await this.ensureOrchestratorKey();
364
+ return { kid: this.orchestratorKey.kid, key: this.orchestratorKey.privateKey, alg: this.opts.orchestratorAlg };
365
+ }
366
+ // ===========================================================================
367
+ // Internals (fetch, rotation, helpers)
368
+ // ===========================================================================
369
+ async tryFetchJwks(providerId, uri) {
370
+ try {
371
+ const jwks = await this.fetchJson(uri);
372
+ if (jwks?.keys?.length) {
373
+ this.setProviderJwks(providerId, jwks);
374
+ return jwks;
375
+ }
376
+ } catch {
377
+ }
378
+ return void 0;
379
+ }
380
+ async tryFetchAsMeta(url) {
381
+ try {
382
+ return await this.fetchJson(url);
383
+ } catch {
384
+ return void 0;
385
+ }
386
+ }
387
+ async fetchJson(url) {
388
+ const ctl = typeof AbortController !== "undefined" ? new AbortController() : void 0;
389
+ const timer = setTimeout(() => ctl?.abort(), this.opts.networkTimeoutMs);
390
+ try {
391
+ let currentUrl = url;
392
+ for (let hop = 0; ; hop++) {
393
+ const parsed = new URL(currentUrl);
394
+ const host = parsed.hostname.toLowerCase();
395
+ const isLocalhost = host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]" || host.endsWith(".localhost");
396
+ if (parsed.protocol !== "https:" && !(parsed.protocol === "http:" && (isLocalhost || !isProduction()))) {
397
+ throw new Error(`insecure scheme "${parsed.protocol}"`);
398
+ }
399
+ if (!(isLocalhost && !isProduction())) {
400
+ const ssrf = await resolveAndCheckHostname(parsed.hostname);
401
+ if (!ssrf.allowed) throw new Error(`blocked host: ${ssrf.reason}`);
402
+ }
403
+ const res = await fetch(currentUrl, {
404
+ method: "GET",
405
+ headers: { accept: "application/json" },
406
+ signal: ctl?.signal,
407
+ // Follow redirects manually so each hop is re-validated (scheme + SSRF).
408
+ redirect: "manual"
409
+ });
410
+ if (isRedirectResponse(res)) {
411
+ const location = res.headers.get("location");
412
+ if (!location) throw new Error("redirect without a readable Location header");
413
+ if (hop >= MAX_JWKS_FETCH_HOPS) throw new Error("too many redirects");
414
+ currentUrl = new URL(location, currentUrl).toString();
415
+ continue;
416
+ }
417
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
418
+ const contentLength = res.headers.get("content-length");
419
+ if (contentLength && Number(contentLength) > MAX_JWKS_FETCH_BYTES) {
420
+ throw new Error("response too large");
421
+ }
422
+ const text = await res.text();
423
+ if (text.length > MAX_JWKS_FETCH_BYTES) {
424
+ throw new Error("response too large");
425
+ }
426
+ return JSON.parse(text);
427
+ }
428
+ } finally {
429
+ clearTimeout(timer);
430
+ }
431
+ }
432
+ async ensureOrchestratorKey() {
433
+ const now = Date.now();
434
+ const maxAge = this.opts.rotateDays * 24 * 60 * 60 * 1e3;
435
+ if (this.orchestratorKey && now - this.orchestratorKey.createdAt <= maxAge) {
436
+ return;
437
+ }
438
+ if (this.keyInitPromise) {
439
+ await this.keyInitPromise;
440
+ return;
441
+ }
442
+ this.keyInitPromise = this.initializeOrchestratorKey(now, maxAge);
443
+ try {
444
+ await this.keyInitPromise;
445
+ } finally {
446
+ this.keyInitPromise = void 0;
447
+ }
448
+ }
449
+ async initializeOrchestratorKey(now, maxAge) {
450
+ const persistence = await this.getKeyPersistence();
451
+ if (persistence && !this.keyInitialized) {
452
+ this.keyInitialized = true;
453
+ const loaded = await persistence.getAsymmetric("jwks-orchestrator");
454
+ if (loaded && now - loaded.createdAt <= maxAge) {
455
+ if (loaded.alg !== this.opts.orchestratorAlg) {
456
+ this.logger.warn(
457
+ "[JwksService] Persisted key algorithm (%s) doesn't match config (%s), generating new key",
458
+ loaded.alg,
459
+ this.opts.orchestratorAlg
460
+ );
461
+ } else {
462
+ try {
463
+ const { createPrivateKey } = __require("node:crypto");
464
+ const privateKey = createPrivateKey({
465
+ key: loaded.privateKey,
466
+ format: "jwk"
467
+ });
468
+ this.orchestratorKey = {
469
+ kid: loaded.kid,
470
+ privateKey,
471
+ publicJwk: loaded.publicJwk,
472
+ createdAt: loaded.createdAt
473
+ };
474
+ return;
475
+ } catch (error) {
476
+ this.logger.warn(
477
+ "[JwksService] Failed to load persisted key: %s, generating new key",
478
+ error.message
479
+ );
480
+ }
481
+ }
482
+ }
483
+ }
484
+ this.orchestratorKey = this.generateKey(this.opts.orchestratorAlg);
485
+ this.keyInitialized = true;
486
+ if (persistence) {
487
+ try {
488
+ await persistence.set({
489
+ type: "asymmetric",
490
+ kid: this.orchestratorKey.kid,
491
+ alg: this.opts.orchestratorAlg,
492
+ privateKey: this.orchestratorKey.privateKey.export({ format: "jwk" }),
493
+ publicJwk: this.orchestratorKey.publicJwk,
494
+ createdAt: this.orchestratorKey.createdAt,
495
+ version: 1
496
+ });
497
+ } catch (error) {
498
+ this.logger.warn("[JwksService] Failed to persist dev key: %s", error.message);
499
+ }
500
+ }
501
+ }
502
+ generateKey(alg) {
503
+ const { generateKeyPairSync } = __require("node:crypto");
504
+ if (alg === "RS256") {
505
+ const { privateKey, publicKey } = generateKeyPairSync("rsa", { modulusLength: 2048 });
506
+ const kid = bytesToHex(randomBytes(8));
507
+ const publicJwk = publicKey.export({ format: "jwk" });
508
+ Object.assign(publicJwk, { kid, alg: "RS256", use: "sig", kty: "RSA" });
509
+ return { kid, privateKey, publicJwk: { keys: [publicJwk] }, createdAt: Date.now() };
510
+ } else {
511
+ const { privateKey, publicKey } = generateKeyPairSync("ec", { namedCurve: "P-256" });
512
+ const kid = bytesToHex(randomBytes(8));
513
+ const publicJwk = publicKey.export({ format: "jwk" });
514
+ Object.assign(publicJwk, { kid, alg: "ES256", use: "sig", kty: "EC" });
515
+ return { kid, privateKey, publicJwk: { keys: [publicJwk] }, createdAt: Date.now() };
516
+ }
517
+ }
518
+ };
519
+ export {
520
+ JwksService
521
+ };
@@ -0,0 +1,33 @@
1
+ // libs/auth/src/jwks/jwks.utils.ts
2
+ function trimSlash(s) {
3
+ const str = s ?? "";
4
+ let end = str.length;
5
+ while (end > 0 && str[end - 1] === "/") {
6
+ end--;
7
+ }
8
+ return str.slice(0, end);
9
+ }
10
+ function normalizeIssuer(u) {
11
+ return trimSlash(String(u ?? ""));
12
+ }
13
+ function decodeJwtPayloadSafe(token) {
14
+ if (!token) return void 0;
15
+ const parts = token.split(".");
16
+ if (parts.length < 2) return void 0;
17
+ try {
18
+ const b64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
19
+ const json = typeof Buffer !== "undefined" ? Buffer.from(b64, "base64").toString("utf8") : (
20
+ // browser fallback
21
+ atob(b64)
22
+ );
23
+ const obj = JSON.parse(json);
24
+ return obj && typeof obj === "object" && !Array.isArray(obj) ? obj : void 0;
25
+ } catch {
26
+ return void 0;
27
+ }
28
+ }
29
+ export {
30
+ decodeJwtPayloadSafe,
31
+ normalizeIssuer,
32
+ trimSlash
33
+ };
@@ -0,0 +1,18 @@
1
+ // libs/auth/src/options/app-auth.schema.ts
2
+ import { internalZ as z } from "@frontmcp/lazy-zod";
3
+ import { localAuthSchema, remoteAuthSchema } from "./orchestrated.schema.mjs";
4
+ import { publicAuthOptionsSchema } from "./public.schema.mjs";
5
+ import { transparentAuthOptionsSchema } from "./transparent.schema.mjs";
6
+ var standaloneOptionSchema = {
7
+ standalone: z.boolean().optional(),
8
+ excludeFromParent: z.boolean().optional()
9
+ };
10
+ var appAuthOptionsSchema = z.union([
11
+ publicAuthOptionsSchema.extend(standaloneOptionSchema),
12
+ transparentAuthOptionsSchema.extend(standaloneOptionSchema),
13
+ localAuthSchema.extend(standaloneOptionSchema),
14
+ remoteAuthSchema.extend(standaloneOptionSchema)
15
+ ]);
16
+ export {
17
+ appAuthOptionsSchema
18
+ };
@@ -0,0 +1,89 @@
1
+ // libs/auth/src/options/index.ts
2
+ import {
3
+ publicAccessConfigSchema,
4
+ localSigningConfigSchema,
5
+ providerConfigSchema,
6
+ remoteProviderConfigSchema,
7
+ flatRemoteProviderFields,
8
+ tokenStorageConfigSchema,
9
+ tokenStorageSqliteSchema,
10
+ secureStoreConfigSchema,
11
+ secureStoreScopeSchema,
12
+ secureStoreEncryptionSchema,
13
+ secureStoreCustomBackendSchema,
14
+ tokenRefreshConfigSchema,
15
+ skippedAppBehaviorSchema,
16
+ consentConfigSchema,
17
+ federatedAuthConfigSchema,
18
+ incrementalAuthConfigSchema,
19
+ upstreamProviderSchema,
20
+ localDcrClientSchema,
21
+ localDcrConfigSchema
22
+ } from "./shared.schemas.mjs";
23
+ import { publicAuthOptionsSchema } from "./public.schema.mjs";
24
+ import { staticAuthOptionsSchema } from "./static.schema.mjs";
25
+ import { transparentAuthOptionsSchema } from "./transparent.schema.mjs";
26
+ import {
27
+ localAuthSchema,
28
+ remoteAuthSchema,
29
+ loginConfigSchema,
30
+ authenticateFnSchema,
31
+ orchestratedLocalSchema,
32
+ orchestratedRemoteSchema
33
+ } from "./orchestrated.schema.mjs";
34
+ import { authOptionsSchema } from "./schema.mjs";
35
+ import { appAuthOptionsSchema } from "./app-auth.schema.mjs";
36
+ import {
37
+ parseAuthOptions,
38
+ isPublicMode,
39
+ isStaticMode,
40
+ isTransparentMode,
41
+ isLocalMode,
42
+ isRemoteMode,
43
+ isOrchestratedMode,
44
+ isOrchestratedLocal,
45
+ isOrchestratedRemote,
46
+ allowsPublicAccess
47
+ } from "./utils.mjs";
48
+ export {
49
+ allowsPublicAccess,
50
+ appAuthOptionsSchema,
51
+ authOptionsSchema,
52
+ authenticateFnSchema,
53
+ consentConfigSchema,
54
+ federatedAuthConfigSchema,
55
+ flatRemoteProviderFields,
56
+ incrementalAuthConfigSchema,
57
+ isLocalMode,
58
+ isOrchestratedLocal,
59
+ isOrchestratedMode,
60
+ isOrchestratedRemote,
61
+ isPublicMode,
62
+ isRemoteMode,
63
+ isStaticMode,
64
+ isTransparentMode,
65
+ localAuthSchema,
66
+ localDcrClientSchema,
67
+ localDcrConfigSchema,
68
+ localSigningConfigSchema,
69
+ loginConfigSchema,
70
+ orchestratedLocalSchema,
71
+ orchestratedRemoteSchema,
72
+ parseAuthOptions,
73
+ providerConfigSchema,
74
+ publicAccessConfigSchema,
75
+ publicAuthOptionsSchema,
76
+ remoteAuthSchema,
77
+ remoteProviderConfigSchema,
78
+ secureStoreConfigSchema,
79
+ secureStoreCustomBackendSchema,
80
+ secureStoreEncryptionSchema,
81
+ secureStoreScopeSchema,
82
+ skippedAppBehaviorSchema,
83
+ staticAuthOptionsSchema,
84
+ tokenRefreshConfigSchema,
85
+ tokenStorageConfigSchema,
86
+ tokenStorageSqliteSchema,
87
+ transparentAuthOptionsSchema,
88
+ upstreamProviderSchema
89
+ };