@frontmcp/auth 1.11.1 → 1.11.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.md +2 -2
  2. package/authorities/authorities.context.d.ts.map +1 -1
  3. package/authorities/authorities.engine.d.ts +1 -1
  4. package/authorities/authorities.engine.d.ts.map +1 -1
  5. package/authorities/authorities.evaluator.d.ts.map +1 -1
  6. package/authorities/authorities.profiles.d.ts +1 -1
  7. package/authorities/authorities.profiles.d.ts.map +1 -1
  8. package/authorities/authorities.registry.d.ts +1 -1
  9. package/authorities/authorities.registry.d.ts.map +1 -1
  10. package/authorities/authorities.scope-mapping.d.ts +1 -1
  11. package/authorities/authorities.scope-mapping.d.ts.map +1 -1
  12. package/authorities/authorities.types.d.ts +4 -0
  13. package/authorities/authorities.types.d.ts.map +1 -1
  14. package/authorization/orchestrated.accessor.d.ts.map +1 -1
  15. package/authorization/public.authorization.d.ts.map +1 -1
  16. package/authorization/transparent.authorization.d.ts +1 -1
  17. package/authorization/transparent.authorization.d.ts.map +1 -1
  18. package/browser/authorities/authorities.context.mjs +3 -1
  19. package/browser/authorities/authorities.engine.mjs +32 -33
  20. package/browser/authorities/authorities.evaluator.mjs +42 -16
  21. package/browser/authorization/dcr-client.registry.mjs +1 -1
  22. package/browser/authorization/public.authorization.mjs +1 -1
  23. package/browser/authorization/transparent.authorization.mjs +1 -1
  24. package/browser/cimd/cimd-client-assertion.mjs +108 -0
  25. package/browser/cimd/cimd-redis.cache.mjs +5 -3
  26. package/browser/cimd/cimd.errors.mjs +11 -0
  27. package/browser/cimd/cimd.service.mjs +127 -31
  28. package/browser/cimd/cimd.types.mjs +15 -10
  29. package/browser/cimd/index.mjs +13 -1
  30. package/browser/errors/auth-internal.error.mjs +3 -2
  31. package/browser/errors/auth-internal.errors.mjs +8 -1
  32. package/browser/errors/index.mjs +2 -0
  33. package/browser/index.mjs +21 -1
  34. package/browser/jwks/index.mjs +2 -0
  35. package/browser/jwks/jwks.service.mjs +22 -103
  36. package/browser/jwks/local-token-keys.mjs +155 -0
  37. package/browser/options/public.schema.mjs +7 -4
  38. package/browser/options/shared.schemas.mjs +38 -13
  39. package/browser/session/federated-auth.session.mjs +2 -0
  40. package/browser/session/incremental-auth-ticket.mjs +24 -1
  41. package/browser/session/index.mjs +4 -0
  42. package/browser/session/redis-session.store.mjs +3 -3
  43. package/browser/session/session-crypto.mjs +4 -4
  44. package/browser/session/storage/storage-authorization-vault.mjs +3 -1
  45. package/browser/session/token.vault.mjs +1 -1
  46. package/browser/session/utils/index.mjs +9 -1
  47. package/browser/session/utils/session-crypto.utils.mjs +4 -0
  48. package/browser/ui/index.mjs +2 -0
  49. package/browser/ui/templates.mjs +65 -11
  50. package/browser/upstream/index.mjs +6 -0
  51. package/browser/upstream/upstream-client-registration.mjs +273 -0
  52. package/cimd/cimd-client-assertion.d.ts +43 -0
  53. package/cimd/cimd-client-assertion.d.ts.map +1 -0
  54. package/cimd/cimd-redis.cache.d.ts +2 -2
  55. package/cimd/cimd-redis.cache.d.ts.map +1 -1
  56. package/cimd/cimd.cache.d.ts +1 -1
  57. package/cimd/cimd.cache.d.ts.map +1 -1
  58. package/cimd/cimd.errors.d.ts +14 -0
  59. package/cimd/cimd.errors.d.ts.map +1 -1
  60. package/cimd/cimd.service.d.ts +37 -2
  61. package/cimd/cimd.service.d.ts.map +1 -1
  62. package/cimd/cimd.types.d.ts +6 -0
  63. package/cimd/cimd.types.d.ts.map +1 -1
  64. package/cimd/index.d.ts +3 -2
  65. package/cimd/index.d.ts.map +1 -1
  66. package/errors/auth-internal.error.d.ts +4 -5
  67. package/errors/auth-internal.error.d.ts.map +1 -1
  68. package/errors/auth-internal.errors.d.ts +8 -0
  69. package/errors/auth-internal.errors.d.ts.map +1 -1
  70. package/errors/index.d.ts +1 -1
  71. package/errors/index.d.ts.map +1 -1
  72. package/esm/index.mjs +54 -14
  73. package/esm/package.json +5 -5
  74. package/index.d.ts +12 -10
  75. package/index.d.ts.map +1 -1
  76. package/index.js +58 -18
  77. package/jwks/index.d.ts +2 -0
  78. package/jwks/index.d.ts.map +1 -1
  79. package/jwks/jwks.service.d.ts +12 -12
  80. package/jwks/jwks.service.d.ts.map +1 -1
  81. package/jwks/jwks.types.d.ts +2 -0
  82. package/jwks/jwks.types.d.ts.map +1 -1
  83. package/jwks/local-token-keys.d.ts +41 -0
  84. package/jwks/local-token-keys.d.ts.map +1 -0
  85. package/options/interfaces.d.ts +64 -15
  86. package/options/interfaces.d.ts.map +1 -1
  87. package/options/public.schema.d.ts.map +1 -1
  88. package/options/shared.schemas.d.ts.map +1 -1
  89. package/package.json +5 -5
  90. package/session/authorization.store.d.ts +9 -2
  91. package/session/authorization.store.d.ts.map +1 -1
  92. package/session/federated-auth.session.d.ts +8 -0
  93. package/session/federated-auth.session.d.ts.map +1 -1
  94. package/session/incremental-auth-ticket.d.ts +10 -0
  95. package/session/incremental-auth-ticket.d.ts.map +1 -1
  96. package/session/index.d.ts +2 -2
  97. package/session/index.d.ts.map +1 -1
  98. package/session/redis-session.store.d.ts +1 -1
  99. package/session/redis-session.store.d.ts.map +1 -1
  100. package/session/session-crypto.d.ts.map +1 -1
  101. package/session/storage/in-memory-authorization-vault.d.ts.map +1 -1
  102. package/session/storage/storage-authorization-vault.d.ts +2 -2
  103. package/session/storage/storage-authorization-vault.d.ts.map +1 -1
  104. package/session/storage/storage-token-store.d.ts +2 -2
  105. package/session/storage/storage-token-store.d.ts.map +1 -1
  106. package/session/token.refresh.d.ts.map +1 -1
  107. package/session/utils/index.d.ts +1 -1
  108. package/session/utils/index.d.ts.map +1 -1
  109. package/session/utils/session-crypto.utils.d.ts +5 -0
  110. package/session/utils/session-crypto.utils.d.ts.map +1 -1
  111. package/ui/index.d.ts +2 -2
  112. package/ui/index.d.ts.map +1 -1
  113. package/ui/templates.d.ts +33 -0
  114. package/ui/templates.d.ts.map +1 -1
  115. package/upstream/index.d.ts +3 -0
  116. package/upstream/index.d.ts.map +1 -0
  117. package/upstream/upstream-client-registration.d.ts +50 -0
  118. package/upstream/upstream-client-registration.d.ts.map +1 -0
  119. package/vault/auth-providers.accessor.d.ts +1 -1
  120. package/vault/auth-providers.accessor.d.ts.map +1 -1
  121. package/vault/auth-providers.registry.d.ts +1 -1
  122. package/vault/auth-providers.registry.d.ts.map +1 -1
  123. package/vault/credential-cache.d.ts +1 -1
  124. package/vault/credential-cache.d.ts.map +1 -1
  125. package/vault/credential-loaders/eager-loader.d.ts +2 -2
  126. package/vault/credential-loaders/eager-loader.d.ts.map +1 -1
@@ -9,14 +9,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
9
9
  });
10
10
 
11
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";
12
+ import { bytesToHex, isProduction, isRedirectResponse, randomBytes, rsaVerify } from "@frontmcp/utils";
20
13
  import { resolveAndCheckHostname } from "../cimd/cimd.validator.mjs";
21
14
  import { noopAuthLogger } from "../common/auth-logger.interface.mjs";
22
15
  import { loadJose } from "./jose.loader.mjs";
@@ -88,16 +81,12 @@ var JwksService = class {
88
81
  warnedProviders = /* @__PURE__ */ new Set();
89
82
  // Orchestrator signing material
90
83
  orchestratorKey;
84
+ // The keys this server's own tokens verify with (see setPublicJwks)
85
+ publicJwks = { keys: [] };
91
86
  // Provider JWKS cache (providerId -> jwks + fetchedAt)
92
87
  providerJwks = /* @__PURE__ */ new Map();
93
88
  // The last key refetch an unknown `kid` started, per provider (see refetchForUnknownKid)
94
89
  unknownKidRefetches = /* @__PURE__ */ new Map();
95
- // Track if key has been initialized (for async loading)
96
- keyInitialized = false;
97
- // Promise guard to prevent concurrent key generation
98
- keyInitPromise;
99
- // KeyPersistence instance for unified key storage
100
- keyPersistence;
101
90
  constructor(opts) {
102
91
  this.logger = opts?.logger ?? noopAuthLogger;
103
92
  this.opts = {
@@ -110,27 +99,15 @@ var JwksService = class {
110
99
  };
111
100
  }
112
101
  // ===========================================================================
113
- // Key Persistence Helpers
114
- // ===========================================================================
115
- /**
116
- * Get or create the KeyPersistence instance.
117
- * Returns null if persistence is disabled (production).
118
- */
119
- async getKeyPersistence() {
120
- if (isProduction()) return null;
121
- if (!this.keyPersistence) {
122
- this.keyPersistence = await createKeyPersistence({
123
- baseDir: ".frontmcp/keys"
124
- });
125
- }
126
- return this.keyPersistence;
127
- }
128
- // ===========================================================================
129
102
  // Public JWKS (what /.well-known/jwks.json serves)
130
103
  // ===========================================================================
131
- /** Gateway's public JWKS (publish at /.well-known/jwks.json when orchestrated). */
104
+ /** The public keys this server's tokens verify with, served at `/.well-known/jwks.json`; empty for HMAC tokens. */
132
105
  async getPublicJwks() {
133
- return this.getOrchestratorJwks();
106
+ return { keys: [...this.publicJwks.keys] };
107
+ }
108
+ /** Set the public keys {@link getPublicJwks} serves (`LocalTokenKeys.publicJwks`). */
109
+ setPublicJwks(jwks) {
110
+ this.publicJwks = { keys: [...jwks.keys] };
134
111
  }
135
112
  // ===========================================================================
136
113
  // Scope-aware verification API
@@ -393,14 +370,20 @@ var JwksService = class {
393
370
  // ===========================================================================
394
371
  // Orchestrator keys (generation/rotation)
395
372
  // ===========================================================================
396
- /** Return the orchestrator public JWKS (generates/rotates as needed). */
373
+ /**
374
+ * Return the orchestrator public JWKS (generates/rotates as needed, in memory).
375
+ * @deprecated FrontMCP never signs with this key; `getPublicJwks()` serves the keys its tokens verify with.
376
+ */
397
377
  async getOrchestratorJwks() {
398
- await this.ensureOrchestratorKey();
378
+ this.ensureOrchestratorKey();
399
379
  return this.orchestratorKey.publicJwk;
400
380
  }
401
- /** Return private signing key + kid for issuing orchestrator tokens. */
381
+ /**
382
+ * Return private signing key + kid for issuing orchestrator tokens.
383
+ * @deprecated FrontMCP never signs with this key; configure `signKey` to sign tokens with your own key.
384
+ */
402
385
  async getOrchestratorSigningKey() {
403
- await this.ensureOrchestratorKey();
386
+ this.ensureOrchestratorKey();
404
387
  return { kid: this.orchestratorKey.kid, key: this.orchestratorKey.privateKey, alg: this.opts.orchestratorAlg };
405
388
  }
406
389
  // ===========================================================================
@@ -469,74 +452,10 @@ var JwksService = class {
469
452
  clearTimeout(timer);
470
453
  }
471
454
  }
472
- async ensureOrchestratorKey() {
473
- const now = Date.now();
455
+ ensureOrchestratorKey() {
474
456
  const maxAge = this.opts.rotateDays * 24 * 60 * 60 * 1e3;
475
- if (this.orchestratorKey && now - this.orchestratorKey.createdAt <= maxAge) {
476
- return;
477
- }
478
- if (this.keyInitPromise) {
479
- await this.keyInitPromise;
480
- return;
481
- }
482
- this.keyInitPromise = this.initializeOrchestratorKey(now, maxAge);
483
- try {
484
- await this.keyInitPromise;
485
- } finally {
486
- this.keyInitPromise = void 0;
487
- }
488
- }
489
- async initializeOrchestratorKey(now, maxAge) {
490
- const persistence = await this.getKeyPersistence();
491
- if (persistence && !this.keyInitialized) {
492
- this.keyInitialized = true;
493
- const loaded = await persistence.getAsymmetric("jwks-orchestrator");
494
- if (loaded && now - loaded.createdAt <= maxAge) {
495
- if (loaded.alg !== this.opts.orchestratorAlg) {
496
- this.logger.warn(
497
- "[JwksService] Persisted key algorithm (%s) doesn't match config (%s), generating new key",
498
- loaded.alg,
499
- this.opts.orchestratorAlg
500
- );
501
- } else {
502
- try {
503
- const { createPrivateKey } = __require("node:crypto");
504
- const privateKey = createPrivateKey({
505
- key: loaded.privateKey,
506
- format: "jwk"
507
- });
508
- this.orchestratorKey = {
509
- kid: loaded.kid,
510
- privateKey,
511
- publicJwk: loaded.publicJwk,
512
- createdAt: loaded.createdAt
513
- };
514
- return;
515
- } catch (error) {
516
- this.logger.warn(
517
- "[JwksService] Failed to load persisted key: %s, generating new key",
518
- error.message
519
- );
520
- }
521
- }
522
- }
523
- }
524
- this.orchestratorKey = this.generateKey(this.opts.orchestratorAlg);
525
- this.keyInitialized = true;
526
- if (persistence) {
527
- try {
528
- await persistence.set({
529
- type: "asymmetric",
530
- kid: this.orchestratorKey.kid,
531
- alg: this.opts.orchestratorAlg,
532
- privateKey: this.orchestratorKey.privateKey.export({ format: "jwk" }),
533
- publicJwk: this.orchestratorKey.publicJwk,
534
- createdAt: this.orchestratorKey.createdAt,
535
- version: 1
536
- });
537
- } catch (error) {
538
- this.logger.warn("[JwksService] Failed to persist dev key: %s", error.message);
539
- }
457
+ if (!this.orchestratorKey || Date.now() - this.orchestratorKey.createdAt > maxAge) {
458
+ this.orchestratorKey = this.generateKey(this.opts.orchestratorAlg);
540
459
  }
541
460
  }
542
461
  generateKey(alg) {
@@ -0,0 +1,155 @@
1
+ // libs/auth/src/jwks/local-token-keys.ts
2
+ import { base64urlDecode, sha256Base64url } from "@frontmcp/utils";
3
+ import { SigningKeyInvalidError } from "../errors/auth-internal.errors.mjs";
4
+ import { loadJose } from "./jose.loader.mjs";
5
+ var HMAC_MIN_BYTES = { HS256: 32, HS384: 48, HS512: 64 };
6
+ var ALGS_BY_KTY = {
7
+ RSA: ["RS256", "RS384", "RS512", "PS256", "PS384", "PS512"],
8
+ EC: ["ES256", "ES384", "ES512"],
9
+ OKP: ["EdDSA", "Ed25519"]
10
+ };
11
+ var EC_ALG_BY_CURVE = { "P-256": "ES256", "P-384": "ES384", "P-521": "ES512" };
12
+ var PUBLIC_MEMBERS = {
13
+ RSA: ["e", "kty", "n"],
14
+ EC: ["crv", "kty", "x", "y"],
15
+ OKP: ["crv", "kty", "x"]
16
+ };
17
+ var MIN_RSA_MODULUS_BYTES = 256;
18
+ function isAsymmetricKty(kty) {
19
+ return kty === "RSA" || kty === "EC" || kty === "OKP";
20
+ }
21
+ var LocalTokenKeys = class _LocalTokenKeys {
22
+ constructor(alg, kid, publicJwks, algorithms, hmacKey, privateJwk) {
23
+ this.alg = alg;
24
+ this.kid = kid;
25
+ this.publicJwks = publicJwks;
26
+ this.algorithms = algorithms;
27
+ this.hmacKey = hmacKey;
28
+ this.privateJwk = privateJwk;
29
+ }
30
+ alg;
31
+ kid;
32
+ publicJwks;
33
+ algorithms;
34
+ hmacKey;
35
+ privateJwk;
36
+ importedKey;
37
+ /** The published keys as one JWK set, built once: the set imports each key on its first use and keeps it. */
38
+ publicKeySet;
39
+ /** Throws {@link SigningKeyInvalidError} when a configured key can't sign or be published. */
40
+ static fromConfig(config, fallbackSecret, optionPath) {
41
+ const { signKey, jwks } = config;
42
+ if (signKey !== void 0 && !(signKey instanceof Uint8Array) && signKey.kty !== "oct") {
43
+ return _LocalTokenKeys.asymmetric(signKey, jwks?.keys ?? [], optionPath);
44
+ }
45
+ if (jwks?.keys?.length) {
46
+ const hmacSource = signKey ? `${optionPath}.signKey` : "JWT_SECRET";
47
+ throw new SigningKeyInvalidError(
48
+ `${optionPath}.jwks is set, but tokens are signed with an HMAC key (${hmacSource}), which is never published. Set an RSA, EC or Ed25519 private key as ${optionPath}.signKey.`
49
+ );
50
+ }
51
+ const hmac = _LocalTokenKeys.hmacKeyOf(signKey, fallbackSecret, `${optionPath}.signKey`);
52
+ return new _LocalTokenKeys(hmac.alg, hmac.kid, { keys: [] }, [hmac.alg], hmac.key, void 0);
53
+ }
54
+ protectedHeader() {
55
+ return this.kid ? { alg: this.alg, typ: "JWT", kid: this.kid } : { alg: this.alg, typ: "JWT" };
56
+ }
57
+ /** The key `SignJWT.sign` takes; a private JWK is imported once. */
58
+ signingKey() {
59
+ if (this.hmacKey) return Promise.resolve(this.hmacKey);
60
+ this.importedKey ??= loadJose().then(({ importJWK }) => importJWK(this.privateJwk, this.alg)).catch((error) => {
61
+ this.importedKey = void 0;
62
+ const reason = error instanceof Error ? error.message : String(error);
63
+ throw new SigningKeyInvalidError(`The configured signKey can't be imported: ${reason}`);
64
+ });
65
+ return this.importedKey;
66
+ }
67
+ /** Verify a token's signature, pinned to the algorithms these keys sign with. */
68
+ async verify(token, options) {
69
+ const { createLocalJWKSet, jwtVerify } = await loadJose();
70
+ const verifyOptions = { ...options, algorithms: this.algorithms };
71
+ if (this.hmacKey) return jwtVerify(token, this.hmacKey, verifyOptions);
72
+ this.publicKeySet ??= createLocalJWKSet(this.publicJwks);
73
+ return jwtVerify(token, this.publicKeySet, verifyOptions);
74
+ }
75
+ static asymmetric(signKey, jwksKeys, optionPath) {
76
+ const signing = _LocalTokenKeys.publicKeyOf(signKey, `${optionPath}.signKey`, "private");
77
+ const published = [signing];
78
+ jwksKeys.forEach((key, index) => {
79
+ const publicKey = _LocalTokenKeys.publicKeyOf(key, `${optionPath}.jwks.keys[${index}]`, "public");
80
+ if (!published.some((existing) => existing.kid === publicKey.kid)) published.push(publicKey);
81
+ });
82
+ const algorithms = [...new Set(published.map((key) => key.alg))];
83
+ return new _LocalTokenKeys(signing.alg, signing.kid, { keys: published }, algorithms, void 0, signKey);
84
+ }
85
+ static hmacKeyOf(signKey, fallbackSecret, path) {
86
+ if (signKey === void 0) return { alg: "HS256", key: fallbackSecret };
87
+ if (signKey instanceof Uint8Array) return { alg: "HS256", key: _LocalTokenKeys.longEnough(signKey, "HS256", path) };
88
+ const alg = signKey.alg ?? "HS256";
89
+ if (!(alg in HMAC_MIN_BYTES)) {
90
+ throw new SigningKeyInvalidError(`${path} is an oct key with alg "${alg}"; use HS256, HS384 or HS512.`);
91
+ }
92
+ if (typeof signKey.k !== "string" || signKey.k.length === 0) {
93
+ throw new SigningKeyInvalidError(`${path} is an oct key without "k".`);
94
+ }
95
+ return { alg, kid: signKey.kid, key: _LocalTokenKeys.longEnough(base64urlDecode(signKey.k), alg, path) };
96
+ }
97
+ static longEnough(key, alg, path) {
98
+ const minBytes = HMAC_MIN_BYTES[alg];
99
+ if (key.length < minBytes) {
100
+ throw new SigningKeyInvalidError(
101
+ `${path} is ${key.length} bytes; ${alg} requires at least ${minBytes} (RFC 7518 \xA73.2).`
102
+ );
103
+ }
104
+ return key;
105
+ }
106
+ /** The public half of `key` with its `kid` (else its RFC 7638 thumbprint), `alg` and `use`. */
107
+ static publicKeyOf(key, path, expected) {
108
+ if (!isAsymmetricKty(key.kty)) {
109
+ throw new SigningKeyInvalidError(`${path} has kty "${String(key.kty)}"; use an RSA, EC or OKP key.`);
110
+ }
111
+ const isPrivate = typeof key.d === "string";
112
+ if (expected === "private" && !isPrivate) {
113
+ throw new SigningKeyInvalidError(`${path} is a public key (it has no "d"); it must be the private key.`);
114
+ }
115
+ if (expected === "public" && isPrivate) {
116
+ throw new SigningKeyInvalidError(
117
+ `${path} holds a private key; the jwks are published, so list public keys only.`
118
+ );
119
+ }
120
+ if (key.use !== void 0 && key.use !== "sig") {
121
+ throw new SigningKeyInvalidError(`${path} has use "${key.use}"; a signing key needs use "sig" or none.`);
122
+ }
123
+ const alg = key.alg ?? _LocalTokenKeys.defaultAlg(key);
124
+ if (!ALGS_BY_KTY[key.kty].includes(alg)) {
125
+ throw new SigningKeyInvalidError(`${path} has alg "${alg}", which ${key.kty} keys can't sign with.`);
126
+ }
127
+ if (key.kty === "EC" && EC_ALG_BY_CURVE[key.crv ?? ""] !== alg) {
128
+ throw new SigningKeyInvalidError(
129
+ `${path} has curve "${String(key.crv)}" and alg "${alg}"; use P-256/ES256, P-384/ES384 or P-521/ES512.`
130
+ );
131
+ }
132
+ if (key.kty === "OKP" && key.crv !== "Ed25519") {
133
+ throw new SigningKeyInvalidError(`${path} is an OKP key on "${String(key.crv)}"; only Ed25519 signs.`);
134
+ }
135
+ const members = PUBLIC_MEMBERS[key.kty];
136
+ const missing = members.filter((member) => typeof key[member] !== "string");
137
+ if (missing.length > 0) {
138
+ throw new SigningKeyInvalidError(`${path} is missing ${missing.map((member) => `"${member}"`).join(", ")}.`);
139
+ }
140
+ if (key.kty === "RSA" && base64urlDecode(key.n).length < MIN_RSA_MODULUS_BYTES) {
141
+ throw new SigningKeyInvalidError(`${path} is an RSA key shorter than 2048 bits.`);
142
+ }
143
+ const publicMembers = Object.fromEntries(members.map((member) => [member, key[member]]));
144
+ const kid = key.kid ?? sha256Base64url(JSON.stringify(publicMembers));
145
+ return { ...publicMembers, kid, alg, use: "sig" };
146
+ }
147
+ static defaultAlg(key) {
148
+ if (key.kty === "RSA") return "RS256";
149
+ if (key.kty === "OKP") return "EdDSA";
150
+ return EC_ALG_BY_CURVE[key.crv ?? ""] ?? "ES256";
151
+ }
152
+ };
153
+ export {
154
+ LocalTokenKeys
155
+ };
@@ -24,13 +24,16 @@ var publicAuthOptionsSchema = z.object({
24
24
  */
25
25
  publicAccess: publicAccessConfigSchema.optional(),
26
26
  /**
27
- * JWKS for token verification
28
- * @default auto-generated
27
+ * Public keys published at `/.well-known/jwks.json` beside the `signKey`'s, and accepted on verification
28
+ * (e.g. a retired key's, while its tokens expire). Needs an RSA, EC or Ed25519 `signKey`.
29
+ * @default the `signKey`'s public key only
29
30
  */
30
31
  jwks: jsonWebKeySetSchema.optional(),
31
32
  /**
32
- * Private key for signing anonymous tokens
33
- * @default auto-generated
33
+ * Private key to sign tokens with, in place of `JWT_SECRET` (HS256).
34
+ * An RSA, EC or Ed25519 private JWK signs with its `alg` (default by key type) and is published at
35
+ * `/.well-known/jwks.json`; a `Uint8Array` or `oct` JWK is an HMAC key and is never published.
36
+ * @default HS256 with `JWT_SECRET`
34
37
  */
35
38
  signKey: jwkSchema.or(z.instanceof(Uint8Array)).optional()
36
39
  });
@@ -30,13 +30,16 @@ var publicAccessConfigSchema = z.object({
30
30
  });
31
31
  var localSigningConfigSchema = z.object({
32
32
  /**
33
- * Private key for signing tokens
34
- * @default auto-generated
33
+ * Private key to sign tokens with, in place of `JWT_SECRET` (HS256).
34
+ * An RSA, EC or Ed25519 private JWK signs with its `alg` (default by key type) and is published at
35
+ * `/.well-known/jwks.json`; a `Uint8Array` or `oct` JWK is an HMAC key and is never published.
36
+ * @default HS256 with `JWT_SECRET`
35
37
  */
36
38
  signKey: jwkSchema.or(z.instanceof(Uint8Array)).optional(),
37
39
  /**
38
- * JWKS for token verification
39
- * @default auto-generated
40
+ * Public keys published at `/.well-known/jwks.json` beside the `signKey`'s, and accepted on verification
41
+ * (e.g. a retired key's, while its tokens expire). Needs an RSA, EC or Ed25519 `signKey`.
42
+ * @default the `signKey`'s public key only
40
43
  */
41
44
  jwks: jsonWebKeySetSchema.optional(),
42
45
  /**
@@ -80,7 +83,11 @@ var providerConfigSchema = z.object({
80
83
  */
81
84
  verifyIssuer: z.boolean().optional(),
82
85
  /**
83
- * Enable Dynamic Client Registration (DCR)
86
+ * Remote mode: without a `clientId`, register this server with the provider by
87
+ * dynamic client registration (RFC 7591) at startup, and sign in with that client.
88
+ * With a persistent `tokenStorage` the client is kept there (its secret encrypted
89
+ * with `JWT_SECRET`) and reused across restarts and instances. Startup fails when
90
+ * the registration fails. Ignored when `clientId` is set, and in transparent mode.
84
91
  * @default false
85
92
  */
86
93
  dcrEnabled: z.boolean().default(false),
@@ -88,7 +95,10 @@ var providerConfigSchema = z.object({
88
95
  authEndpoint: z.string().url().optional(),
89
96
  /** Token endpoint override */
90
97
  tokenEndpoint: z.string().url().optional(),
91
- /** Registration endpoint override (for DCR) */
98
+ /**
99
+ * Registration endpoint for `dcrEnabled`.
100
+ * @default the `registration_endpoint` of the provider's RFC 8414 or OpenID metadata
101
+ */
92
102
  registrationEndpoint: z.string().url().optional(),
93
103
  /** User info endpoint override */
94
104
  userInfoEndpoint: z.string().url().optional()
@@ -130,7 +140,11 @@ var remoteProviderConfigSchema = z.object({
130
140
  */
131
141
  scopes: z.array(z.string()).optional(),
132
142
  /**
133
- * Enable Dynamic Client Registration (DCR)
143
+ * Remote mode: without a `clientId`, register this server with the provider by
144
+ * dynamic client registration (RFC 7591) at startup, and sign in with that client.
145
+ * With a persistent `tokenStorage` the client is kept there (its secret encrypted
146
+ * with `JWT_SECRET`) and reused across restarts and instances. Startup fails when
147
+ * the registration fails. Ignored when `clientId` is set, and in transparent mode.
134
148
  * @default false
135
149
  */
136
150
  dcrEnabled: z.boolean().default(false),
@@ -143,7 +157,8 @@ var remoteProviderConfigSchema = z.object({
143
157
  */
144
158
  tokenEndpoint: z.string().url().optional(),
145
159
  /**
146
- * Registration endpoint override (for DCR)
160
+ * Registration endpoint for `dcrEnabled`.
161
+ * @default the `registration_endpoint` of the provider's RFC 8414 or OpenID metadata
147
162
  */
148
163
  registrationEndpoint: z.string().url().optional(),
149
164
  /**
@@ -212,11 +227,11 @@ var localDcrClientSchema = z.object({
212
227
  * Token-endpoint auth method. Defaults to `'none'` (public PKCE client).
213
228
  */
214
229
  tokenEndpointAuthMethod: z.enum(["none", "client_secret_basic", "client_secret_post"]).default("none"),
215
- /** Allowed grant types. Defaults to `['authorization_code']`. */
216
- grantTypes: z.array(z.enum(["authorization_code", "refresh_token"])).default(["authorization_code"]),
230
+ /** Allowed grant types; without `refresh_token`, the client gets no refresh token. Defaults to both. */
231
+ grantTypes: z.array(z.enum(["authorization_code", "refresh_token"])).default(["authorization_code", "refresh_token"]),
217
232
  /** Allowed response types. Defaults to `['code']`. */
218
233
  responseTypes: z.array(z.enum(["code"])).default(["code"]),
219
- /** Optional space-delimited default scope string. */
234
+ /** Space-delimited scopes this client may be granted; a request for others is narrowed to these. */
220
235
  scope: z.string().optional()
221
236
  });
222
237
  var localDcrConfigSchema = z.object({
@@ -469,12 +484,22 @@ var incrementalAuthConfigSchema = z.object({
469
484
  */
470
485
  skippedAppBehavior: skippedAppBehaviorSchema.default("anonymous"),
471
486
  /**
472
- * Allow users to skip app authorization during initial auth flow
487
+ * Allow users to skip app authorization during initial auth flow. With
488
+ * `false`, a sign-in through the provider picker must link the provider of
489
+ * every app with its own `auth`: the picker shows them checked and required,
490
+ * and the callbacks refuse a sign-in that leaves one out or declines it.
473
491
  * @default true
474
492
  */
475
493
  allowSkip: z.boolean().default(true),
476
494
  /**
477
- * Show all apps in a single authorization page (vs step-by-step)
495
+ * Show every provider on one picker page (`true`), or one per page (`false`):
496
+ * the sign-in then walks the picker's providers in order, and each step
497
+ * links or skips one of them (with Back to change an answer). Each step is
498
+ * checked server-side: a CSRF token, and `allowSkip` and
499
+ * `federatedAuth.requiredProviders` refuse skipping a required provider.
500
+ * The last step carries the sign-in fields, and finishes the sign-in as the
501
+ * one-page picker does. A custom `@AuthUi({ slot: 'federated' })` page gets
502
+ * the step in `extras.step` (see `FederatedStep`).
478
503
  * @default true
479
504
  */
480
505
  showAllAppsAtOnce: z.boolean().default(true)
@@ -88,6 +88,7 @@ var InMemoryFederatedAuthSessionStore = class {
88
88
  completedProviders: /* @__PURE__ */ new Map(),
89
89
  skippedProviders: [],
90
90
  signinBinding: params.signinBinding,
91
+ authorizedAppIds: params.authorizedAppIds,
91
92
  createdAt: now,
92
93
  expiresAt: now + this.sessionTtlMs
93
94
  };
@@ -121,6 +122,7 @@ function createFederatedAuthSession(params, ttlMs = 15 * 60 * 1e3) {
121
122
  completedProviders: /* @__PURE__ */ new Map(),
122
123
  skippedProviders: [],
123
124
  signinBinding: params.signinBinding,
125
+ authorizedAppIds: params.authorizedAppIds,
124
126
  createdAt: now,
125
127
  expiresAt: now + ttlMs
126
128
  };
@@ -1,13 +1,29 @@
1
1
  // libs/auth/src/session/incremental-auth-ticket.ts
2
2
  import { randomUUID, signData, verifyData } from "@frontmcp/utils";
3
+ import { openPendingLogin, sealPendingLogin } from "./pending-login.mjs";
3
4
  var DEFAULT_INCREMENTAL_TICKET_TTL_MS = 3e5;
5
+ function profileKey(secret) {
6
+ return new TextEncoder().encode(secret);
7
+ }
8
+ function profileBinding(jti) {
9
+ return `incremental-ticket:${jti}`;
10
+ }
4
11
  function signIncrementalAuthTicket(input, secret) {
12
+ const jti = randomUUID();
13
+ const hasProfile = input.email !== void 0 || input.name !== void 0;
5
14
  const payload = {
6
15
  sub: input.sub,
7
16
  appId: input.appId,
8
17
  ...input.toolId !== void 0 ? { toolId: input.toolId } : {},
9
18
  ...input.priorAppIds !== void 0 ? { priorAppIds: input.priorAppIds } : {},
10
- jti: randomUUID(),
19
+ ...hasProfile ? {
20
+ profile: sealPendingLogin(profileKey(secret), profileBinding(jti), {
21
+ sub: input.sub,
22
+ email: input.email,
23
+ name: input.name
24
+ })
25
+ } : {},
26
+ jti,
11
27
  exp: Date.now() + (input.ttlMs ?? DEFAULT_INCREMENTAL_TICKET_TTL_MS)
12
28
  };
13
29
  const signedJson = signData(payload, { secret });
@@ -27,6 +43,7 @@ function verifyIncrementalAuthTicket(token, secret, now = Date.now()) {
27
43
  if (typeof payload.jti !== "string" || payload.jti.length === 0) return null;
28
44
  if (!Number.isFinite(payload.exp)) return null;
29
45
  if (payload.toolId !== void 0 && typeof payload.toolId !== "string") return null;
46
+ if (payload.profile !== void 0 && typeof payload.profile !== "string") return null;
30
47
  if (payload.priorAppIds !== void 0 && (!Array.isArray(payload.priorAppIds) || payload.priorAppIds.some((id) => typeof id !== "string"))) {
31
48
  return null;
32
49
  }
@@ -35,8 +52,14 @@ function verifyIncrementalAuthTicket(token, secret, now = Date.now()) {
35
52
  }
36
53
  return payload;
37
54
  }
55
+ function openIncrementalAuthTicketProfile(payload, secret) {
56
+ if (!payload.profile) return void 0;
57
+ const login = openPendingLogin(profileKey(secret), profileBinding(payload.jti), payload.profile);
58
+ return login?.sub === payload.sub ? { sub: login.sub, email: login.email, name: login.name } : void 0;
59
+ }
38
60
  export {
39
61
  DEFAULT_INCREMENTAL_TICKET_TTL_MS,
62
+ openIncrementalAuthTicketProfile,
40
63
  signIncrementalAuthTicket,
41
64
  verifyIncrementalAuthTicket
42
65
  };
@@ -55,6 +55,7 @@ import {
55
55
  import {
56
56
  signIncrementalAuthTicket,
57
57
  verifyIncrementalAuthTicket,
58
+ openIncrementalAuthTicketProfile,
58
59
  DEFAULT_INCREMENTAL_TICKET_TTL_MS
59
60
  } from "./incremental-auth-ticket.mjs";
60
61
  import { CREDENTIALS_ACCESSOR } from "./credentials-accessor.mjs";
@@ -100,6 +101,7 @@ import {
100
101
  decryptSessionJson,
101
102
  safeDecrypt,
102
103
  resetCachedKey,
104
+ isSessionSecretConfigured,
103
105
  TinyTtlCache
104
106
  } from "./utils/index.mjs";
105
107
  import {
@@ -219,6 +221,7 @@ export {
219
221
  isPersistentTokenStorage,
220
222
  isRedisTokenStorage,
221
223
  isSessionComplete,
224
+ isSessionSecretConfigured,
222
225
  isSignedSession,
223
226
  isSoonExpiring,
224
227
  isSoonExpiringProvider,
@@ -226,6 +229,7 @@ export {
226
229
  legacySseTransportStateSchema,
227
230
  mtlsCredentialSchema,
228
231
  oauthCredentialSchema,
232
+ openIncrementalAuthTicketProfile,
229
233
  openPendingLogin,
230
234
  pendingIncrementalAuthSchema,
231
235
  pkceChallengeSchema,
@@ -1,11 +1,11 @@
1
1
  // libs/auth/src/session/redis-session.store.ts
2
2
  import { randomUUID, RedisStorageAdapter } from "@frontmcp/utils";
3
+ import { SessionIdEmptyError } from "../errors/auth-internal.errors.mjs";
4
+ import { signSession, verifyOrParseSession } from "./session-crypto.mjs";
5
+ import { SessionRateLimiter } from "./session-rate-limiter.mjs";
3
6
  import {
4
7
  storedSessionSchema
5
8
  } from "./transport-session.types.mjs";
6
- import { signSession, verifyOrParseSession } from "./session-crypto.mjs";
7
- import { SessionRateLimiter } from "./session-rate-limiter.mjs";
8
- import { SessionIdEmptyError } from "../errors/auth-internal.errors.mjs";
9
9
  var RedisSessionStore = class {
10
10
  storage;
11
11
  keyPrefix;
@@ -1,11 +1,11 @@
1
1
  // libs/auth/src/session/session-crypto.ts
2
2
  import {
3
+ getEnv,
4
+ isProduction,
5
+ isSignedData,
3
6
  signData,
4
7
  verifyData,
5
- isSignedData,
6
- verifyOrParseData,
7
- getEnv,
8
- isProduction
8
+ verifyOrParseData
9
9
  } from "@frontmcp/utils";
10
10
  import { SessionSecretRequiredError } from "../errors/auth-internal.errors.mjs";
11
11
  function getSigningSecret(config) {
@@ -1,6 +1,8 @@
1
1
  // libs/auth/src/session/storage/storage-authorization-vault.ts
2
2
  import { randomUUID, TypedStorage } from "@frontmcp/utils";
3
- import { authorizationVaultEntrySchema } from "../authorization-vault.mjs";
3
+ import {
4
+ authorizationVaultEntrySchema
5
+ } from "../authorization-vault.mjs";
4
6
  var StorageAuthorizationVault = class {
5
7
  storage;
6
8
  namespace;
@@ -1,5 +1,5 @@
1
1
  // libs/auth/src/session/token.vault.ts
2
- import { encryptAesGcm, decryptAesGcm, randomBytes, base64urlEncode, base64urlDecode } from "@frontmcp/utils";
2
+ import { base64urlDecode, base64urlEncode, decryptAesGcm, encryptAesGcm, randomBytes } from "@frontmcp/utils";
3
3
  var TokenVault = class {
4
4
  /** Active key used for new encryptions */
5
5
  active;
@@ -7,7 +7,14 @@ import {
7
7
  anonymousCallerClaims,
8
8
  extractBearerToken
9
9
  } from "./auth-token.utils.mjs";
10
- import { getKey, encryptJson, decryptSessionJson, safeDecrypt, resetCachedKey } from "./session-crypto.utils.mjs";
10
+ import {
11
+ getKey,
12
+ encryptJson,
13
+ decryptSessionJson,
14
+ safeDecrypt,
15
+ resetCachedKey,
16
+ isSessionSecretConfigured
17
+ } from "./session-crypto.utils.mjs";
11
18
  export {
12
19
  TinyTtlCache,
13
20
  anonymousCallerClaims,
@@ -18,6 +25,7 @@ export {
18
25
  getKey,
19
26
  getTokenSignatureFingerprint,
20
27
  isJwt,
28
+ isSessionSecretConfigured,
21
29
  resetCachedKey,
22
30
  safeDecrypt
23
31
  };
@@ -17,6 +17,9 @@ function getKey() {
17
17
  cachedKey = sha256(new TextEncoder().encode(base));
18
18
  return cachedKey;
19
19
  }
20
+ function isSessionSecretConfigured() {
21
+ return !!getEnv("MCP_SESSION_SECRET");
22
+ }
20
23
  function encryptJson(obj) {
21
24
  const key = getKey();
22
25
  const encrypted = encryptValue(obj, key);
@@ -44,6 +47,7 @@ export {
44
47
  decryptSessionJson,
45
48
  encryptJson,
46
49
  getKey,
50
+ isSessionSecretConfigured,
47
51
  resetCachedKey,
48
52
  safeDecrypt
49
53
  };
@@ -16,6 +16,7 @@ import {
16
16
  buildConsentPage,
17
17
  buildIncrementalAuthPage,
18
18
  buildFederatedLoginPage,
19
+ buildFederatedStepPage,
19
20
  buildToolConsentPage,
20
21
  buildLoginPage,
21
22
  buildConnectPage,
@@ -35,6 +36,7 @@ export {
35
36
  buildConsentPage,
36
37
  buildErrorPage,
37
38
  buildFederatedLoginPage,
39
+ buildFederatedStepPage,
38
40
  buildIncrementalAuthPage,
39
41
  buildLoginPage,
40
42
  buildToolConsentPage,