@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,252 @@
1
+ // libs/auth/src/authorization/authorization.class.ts
2
+ import { base64urlDecode, getMachineId, randomUUID } from "@frontmcp/utils";
3
+ import { TokenLeakDetectedError } from "../errors/auth-internal.errors.mjs";
4
+ import { encryptJson } from "../session/utils/session-crypto.utils.mjs";
5
+ var AuthorizationBase = class _AuthorizationBase {
6
+ id;
7
+ isAnonymous;
8
+ user;
9
+ claims;
10
+ expiresAt;
11
+ scopes;
12
+ authorizedProviders;
13
+ authorizedProviderIds;
14
+ authorizedApps;
15
+ authorizedAppIds;
16
+ authorizedTools;
17
+ authorizedToolIds;
18
+ authorizedPrompts;
19
+ authorizedPromptIds;
20
+ authorizedResources;
21
+ /** The original bearer token (for transparent mode) */
22
+ token;
23
+ /** Active transport sessions for this authorization */
24
+ #sessions = /* @__PURE__ */ new Map();
25
+ /** Creation timestamp */
26
+ createdAt;
27
+ constructor(ctx) {
28
+ this.id = ctx.id;
29
+ this.isAnonymous = ctx.isAnonymous;
30
+ this.user = ctx.user;
31
+ this.claims = ctx.claims;
32
+ this.expiresAt = ctx.expiresAt;
33
+ this.scopes = ctx.scopes ?? [];
34
+ this.token = ctx.token;
35
+ this.createdAt = Date.now();
36
+ this.authorizedProviders = ctx.authorizedProviders ?? {};
37
+ this.authorizedProviderIds = ctx.authorizedProviderIds ?? Object.keys(this.authorizedProviders);
38
+ this.authorizedApps = ctx.authorizedApps ?? {};
39
+ this.authorizedAppIds = ctx.authorizedAppIds ?? Object.keys(this.authorizedApps);
40
+ this.authorizedTools = ctx.authorizedTools ?? {};
41
+ this.authorizedToolIds = ctx.authorizedToolIds ?? Object.keys(this.authorizedTools);
42
+ this.authorizedPrompts = ctx.authorizedPrompts ?? {};
43
+ this.authorizedPromptIds = ctx.authorizedPromptIds ?? Object.keys(this.authorizedPrompts);
44
+ this.authorizedResources = ctx.authorizedResources ?? [];
45
+ }
46
+ /**
47
+ * Create a new transport session for this authorization
48
+ * @param protocol - Transport protocol (sse, streamable-http, etc.)
49
+ * @param fingerprint - Optional client fingerprint for tracking
50
+ */
51
+ createTransportSession(protocol, fingerprint) {
52
+ const sessionId = randomUUID();
53
+ const session = {
54
+ id: sessionId,
55
+ authorizationId: this.id,
56
+ protocol,
57
+ createdAt: Date.now(),
58
+ expiresAt: this.expiresAt,
59
+ nodeId: getMachineId(),
60
+ clientFingerprint: fingerprint
61
+ };
62
+ this.#sessions.set(session.id, session);
63
+ return session;
64
+ }
65
+ /**
66
+ * Get existing transport session by ID
67
+ */
68
+ getTransportSession(sessionId) {
69
+ return this.#sessions.get(sessionId);
70
+ }
71
+ /**
72
+ * Get all active transport sessions
73
+ */
74
+ getAllSessions() {
75
+ return Array.from(this.#sessions.values());
76
+ }
77
+ /**
78
+ * Remove a transport session
79
+ */
80
+ removeTransportSession(sessionId) {
81
+ return this.#sessions.delete(sessionId);
82
+ }
83
+ /**
84
+ * Get count of active sessions
85
+ */
86
+ get sessionCount() {
87
+ return this.#sessions.size;
88
+ }
89
+ /**
90
+ * Check if a scope is granted
91
+ */
92
+ hasScope(scope) {
93
+ return this.scopes.includes(scope);
94
+ }
95
+ /**
96
+ * Check if all scopes are granted
97
+ */
98
+ hasAllScopes(scopes) {
99
+ return scopes.every((s) => this.scopes.includes(s));
100
+ }
101
+ /**
102
+ * Check if any scope is granted
103
+ */
104
+ hasAnyScope(scopes) {
105
+ return scopes.some((s) => this.scopes.includes(s));
106
+ }
107
+ /**
108
+ * Check if a tool is authorized
109
+ */
110
+ canAccessTool(toolId) {
111
+ return toolId in this.authorizedTools || this.authorizedToolIds.includes(toolId);
112
+ }
113
+ /**
114
+ * Check if a prompt is authorized
115
+ */
116
+ canAccessPrompt(promptId) {
117
+ return promptId in this.authorizedPrompts || this.authorizedPromptIds.includes(promptId);
118
+ }
119
+ /**
120
+ * Check if an app is authorized.
121
+ * Used for progressive authorization to determine if tools from this app can be executed.
122
+ * @param appId - App ID to check
123
+ */
124
+ isAppAuthorized(appId) {
125
+ return appId in this.authorizedApps || this.authorizedAppIds.includes(appId);
126
+ }
127
+ /**
128
+ * Build URL for progressive/incremental authorization.
129
+ * Used when a tool requires authorization for an app that was skipped during initial auth.
130
+ *
131
+ * The URL only SKIPS the login step when it carries a valid incremental
132
+ * ticket (GHSA-2c4g-9c8x-6m8g) \u2014 mint one with
133
+ * {@link signIncrementalAuthTicket} using the server signing secret and pass
134
+ * it here. Without a ticket the link still works, but the user completes an
135
+ * ordinary login; `mode=incremental` alone is no longer an authorization
136
+ * signal and is not emitted.
137
+ *
138
+ * @param appId - App ID that requires authorization
139
+ * @param baseUrl - Base URL of the server
140
+ * @param ticket - Signed incremental-authorization ticket for this subject
141
+ */
142
+ getProgressiveAuthUrl(appId, baseUrl, ticket) {
143
+ const params = new URLSearchParams({ app: appId });
144
+ if (ticket) params.set("ticket", ticket);
145
+ return `${baseUrl}/oauth/authorize?${params.toString()}`;
146
+ }
147
+ /**
148
+ * Check if the authorization is expired
149
+ */
150
+ isExpired() {
151
+ if (!this.expiresAt) return false;
152
+ return Date.now() > this.expiresAt;
153
+ }
154
+ /**
155
+ * Get time until expiration in milliseconds
156
+ * Returns undefined if no expiration, negative if expired
157
+ */
158
+ getTimeToExpiry() {
159
+ if (!this.expiresAt) return void 0;
160
+ return this.expiresAt - Date.now();
161
+ }
162
+ /**
163
+ * Convert a transport session to encrypted session JWT
164
+ * This is what gets sent in the Mcp-Session-Id header
165
+ */
166
+ toSessionJwt(session) {
167
+ const payload = {
168
+ sid: session.id,
169
+ aid: this.id,
170
+ proto: session.protocol,
171
+ nid: session.nodeId,
172
+ iat: Math.floor(Date.now() / 1e3),
173
+ exp: this.expiresAt ? Math.floor(this.expiresAt / 1e3) : void 0
174
+ };
175
+ return encryptJson(payload);
176
+ }
177
+ /**
178
+ * Convert to LLM-safe context (no tokens exposed)
179
+ */
180
+ toLLMSafeContext(session) {
181
+ return {
182
+ authorizationId: this.id,
183
+ sessionId: session.id,
184
+ mode: this.mode,
185
+ isAnonymous: this.isAnonymous,
186
+ user: {
187
+ sub: this.user.sub,
188
+ name: this.user.name
189
+ },
190
+ scopes: this.scopes,
191
+ authorizedToolIds: this.authorizedToolIds,
192
+ authorizedPromptIds: this.authorizedPromptIds
193
+ };
194
+ }
195
+ /**
196
+ * Check if a base64url segment decodes to a JWT-like header (contains "alg").
197
+ */
198
+ static isJwtHeader(segment) {
199
+ try {
200
+ const bytes = base64urlDecode(segment);
201
+ const text = new TextDecoder().decode(bytes);
202
+ const parsed = JSON.parse(text);
203
+ return typeof parsed === "object" && parsed !== null && typeof parsed.alg === "string";
204
+ } catch {
205
+ return false;
206
+ }
207
+ }
208
+ /**
209
+ * Validate that no tokens are leaked in data
210
+ * Throws if JWT pattern detected
211
+ */
212
+ static validateNoTokenLeakage(data) {
213
+ const json = JSON.stringify(data);
214
+ const B64URL = /^[A-Za-z0-9_-]+$/;
215
+ const MIN_SEG = 2;
216
+ let dotIdx = json.indexOf(".");
217
+ while (dotIdx !== -1) {
218
+ let seg1Start = dotIdx;
219
+ while (seg1Start > 0 && /[A-Za-z0-9_-]/.test(json[seg1Start - 1])) seg1Start--;
220
+ const seg1Len = dotIdx - seg1Start;
221
+ if (seg1Len >= MIN_SEG) {
222
+ let seg2End = dotIdx + 1;
223
+ while (seg2End < json.length && /[A-Za-z0-9_-]/.test(json[seg2End])) seg2End++;
224
+ const seg2Len = seg2End - (dotIdx + 1);
225
+ if (seg2Len >= MIN_SEG && seg2End < json.length && json[seg2End] === ".") {
226
+ const dot2 = seg2End;
227
+ let seg3End = dot2 + 1;
228
+ while (seg3End < json.length && /[A-Za-z0-9_-]/.test(json[seg3End])) seg3End++;
229
+ const seg3Len = seg3End - (dot2 + 1);
230
+ if (seg3Len >= MIN_SEG) {
231
+ const seg1 = json.substring(seg1Start, dotIdx);
232
+ const seg2 = json.substring(dotIdx + 1, dot2);
233
+ const seg3 = json.substring(dot2 + 1, seg3End);
234
+ if (B64URL.test(seg1) && B64URL.test(seg2) && B64URL.test(seg3) && _AuthorizationBase.isJwtHeader(seg1)) {
235
+ throw new TokenLeakDetectedError("JWT pattern detected in LLM context data");
236
+ }
237
+ }
238
+ }
239
+ }
240
+ dotIdx = json.indexOf(".", dotIdx + 1);
241
+ }
242
+ const sensitiveFields = ["access_token", "refresh_token", "id_token", "tokenEnc", "secretRefId"];
243
+ for (const field of sensitiveFields) {
244
+ if (json.includes(`"${field}"`)) {
245
+ throw new TokenLeakDetectedError(`Sensitive field "${field}" found in data`);
246
+ }
247
+ }
248
+ }
249
+ };
250
+ export {
251
+ AuthorizationBase
252
+ };
@@ -0,0 +1,64 @@
1
+ // libs/auth/src/authorization/authorization.types.ts
2
+ import { internalZ as z } from "@frontmcp/lazy-zod";
3
+ var authModeSchema = z.enum(["public", "transparent", "orchestrated"]);
4
+ var authUserSchema = z.object({
5
+ sub: z.string(),
6
+ name: z.string().optional(),
7
+ email: z.string().email().optional(),
8
+ picture: z.string().url().optional(),
9
+ anonymous: z.boolean().optional()
10
+ });
11
+ var authorizedToolSchema = z.object({
12
+ executionPath: z.tuple([z.string(), z.string()]),
13
+ scopes: z.array(z.string()).optional(),
14
+ details: z.record(z.string(), z.unknown()).optional()
15
+ });
16
+ var authorizedPromptSchema = z.object({
17
+ executionPath: z.tuple([z.string(), z.string()]),
18
+ scopes: z.array(z.string()).optional(),
19
+ details: z.record(z.string(), z.unknown()).optional()
20
+ });
21
+ var llmSafeAuthContextSchema = z.object({
22
+ authorizationId: z.string(),
23
+ sessionId: z.string(),
24
+ mode: authModeSchema,
25
+ isAnonymous: z.boolean(),
26
+ user: z.object({
27
+ sub: z.string(),
28
+ name: z.string().optional()
29
+ }),
30
+ scopes: z.array(z.string()),
31
+ authorizedToolIds: z.array(z.string()),
32
+ authorizedPromptIds: z.array(z.string())
33
+ });
34
+ var AppAuthState = /* @__PURE__ */ ((AppAuthState2) => {
35
+ AppAuthState2["AUTHORIZED"] = "authorized";
36
+ AppAuthState2["SKIPPED"] = "skipped";
37
+ AppAuthState2["PENDING"] = "pending";
38
+ return AppAuthState2;
39
+ })(AppAuthState || {});
40
+ var appAuthStateSchema = z.nativeEnum(AppAuthState);
41
+ var appAuthorizationRecordSchema = z.object({
42
+ appId: z.string(),
43
+ state: appAuthStateSchema,
44
+ stateChangedAt: z.number(),
45
+ grantedScopes: z.array(z.string()).optional(),
46
+ authProviderId: z.string().optional(),
47
+ toolIds: z.array(z.string())
48
+ });
49
+ var progressiveAuthStateSchema = z.object({
50
+ apps: z.record(z.string(), appAuthorizationRecordSchema),
51
+ initiallyAuthorized: z.array(z.string()),
52
+ initiallySkipped: z.array(z.string())
53
+ });
54
+ export {
55
+ AppAuthState,
56
+ appAuthStateSchema,
57
+ appAuthorizationRecordSchema,
58
+ authModeSchema,
59
+ authUserSchema,
60
+ authorizedPromptSchema,
61
+ authorizedToolSchema,
62
+ llmSafeAuthContextSchema,
63
+ progressiveAuthStateSchema
64
+ };
@@ -0,0 +1,222 @@
1
+ // libs/auth/src/authorization/dcr-client.registry.ts
2
+ import { sha256, timingSafeEqual } from "@frontmcp/utils";
3
+ var DEFAULT_MAX_DYNAMIC_CLIENTS = 1e3;
4
+ function globMatch(pattern, input) {
5
+ let p = 0;
6
+ let i = 0;
7
+ let starIdx = -1;
8
+ let matchIdx = 0;
9
+ while (i < input.length) {
10
+ if (p < pattern.length && pattern[p] === input[i]) {
11
+ p++;
12
+ i++;
13
+ } else if (p < pattern.length && pattern[p] === "*") {
14
+ starIdx = p;
15
+ matchIdx = i;
16
+ p++;
17
+ } else if (starIdx !== -1) {
18
+ p = starIdx + 1;
19
+ matchIdx++;
20
+ i = matchIdx;
21
+ } else {
22
+ return false;
23
+ }
24
+ }
25
+ while (p < pattern.length && pattern[p] === "*") p++;
26
+ return p === pattern.length;
27
+ }
28
+ function splitRedirectUri(s) {
29
+ const schemeSep = s.indexOf("://");
30
+ if (schemeSep === -1) return null;
31
+ const scheme = s.slice(0, schemeSep);
32
+ const afterScheme = s.slice(schemeSep + 3);
33
+ let boundary = afterScheme.length;
34
+ for (let i = 0; i < afterScheme.length; i++) {
35
+ const ch = afterScheme[i];
36
+ if (ch === "/" || ch === "?" || ch === "#") {
37
+ boundary = i;
38
+ break;
39
+ }
40
+ }
41
+ const authority = afterScheme.slice(0, boundary);
42
+ const rest = afterScheme.slice(boundary);
43
+ const at = authority.lastIndexOf("@");
44
+ const userinfo = at === -1 ? "" : authority.slice(0, at);
45
+ const hostport = at === -1 ? authority : authority.slice(at + 1);
46
+ let host = hostport;
47
+ let port = "";
48
+ if (hostport.startsWith("[")) {
49
+ const close = hostport.indexOf("]");
50
+ if (close !== -1) {
51
+ host = hostport.slice(0, close + 1);
52
+ if (hostport[close + 1] === ":") port = hostport.slice(close + 2);
53
+ }
54
+ } else {
55
+ const colon = hostport.indexOf(":");
56
+ if (colon !== -1) {
57
+ host = hostport.slice(0, colon);
58
+ port = hostport.slice(colon + 1);
59
+ }
60
+ }
61
+ return { scheme, userinfo, host, port, rest };
62
+ }
63
+ function globMatchRedirectUri(pattern, input) {
64
+ const p = splitRedirectUri(pattern);
65
+ if (!p) return globMatch(pattern, input);
66
+ const u = splitRedirectUri(input);
67
+ if (!u) return false;
68
+ return globMatch(p.scheme, u.scheme) && globMatch(p.userinfo, u.userinfo) && globMatch(p.host, u.host) && globMatch(p.port, u.port) && globMatch(p.rest, u.rest);
69
+ }
70
+ var DcrClientRegistry = class {
71
+ constructor(config = {}) {
72
+ this.config = config;
73
+ this.redirectGlobs = config.allowedRedirectUris;
74
+ this.seedPreRegisteredClients();
75
+ }
76
+ config;
77
+ clients = /* @__PURE__ */ new Map();
78
+ /** Configured redirect_uri allowlist globs (matched via `globMatch`). */
79
+ redirectGlobs;
80
+ /** Seed `dcr.clients` so the authorize/token flows accept them without DCR. */
81
+ seedPreRegisteredClients() {
82
+ const clients = this.config.clients ?? [];
83
+ const now = Math.floor(Date.now() / 1e3);
84
+ for (const c of clients) {
85
+ this.clients.set(c.clientId, {
86
+ client_id: c.clientId,
87
+ client_secret: c.clientSecret,
88
+ token_endpoint_auth_method: c.tokenEndpointAuthMethod ?? "none",
89
+ grant_types: c.grantTypes ?? ["authorization_code"],
90
+ response_types: c.responseTypes ?? ["code"],
91
+ redirect_uris: c.redirectUris,
92
+ client_name: c.clientName,
93
+ scope: c.scope,
94
+ created_at: now,
95
+ dev: false,
96
+ preRegistered: true
97
+ });
98
+ }
99
+ }
100
+ /** Look up a client by id (pre-registered or dynamically registered). */
101
+ get(clientId) {
102
+ return this.clients.get(clientId);
103
+ }
104
+ /**
105
+ * Authenticate a client at the token endpoint (RFC 6749 \u00a72.3 / \u00a73.2.1).
106
+ *
107
+ * - `'unknown'` \u2014 the client id is not registered (caller decides; today's
108
+ * public / CIMD clients are unregistered and treated as "no secret to
109
+ * verify").
110
+ * - `'ok'` \u2014 public client (`none`), or a confidential client whose
111
+ * presented secret matched (constant-time).
112
+ * - `'invalid'` \u2014 a confidential client with a missing or wrong secret. The
113
+ * caller MUST reject with `invalid_client`.
114
+ *
115
+ * SECURITY: previously the token endpoint verified only `client_id` equality,
116
+ * so a DCR-minted `client_secret` was never checked and a "confidential"
117
+ * client was effectively public. This restores confidential-client
118
+ * authentication with a timing-safe comparison.
119
+ */
120
+ verifyClientSecret(clientId, presentedSecret) {
121
+ const client = this.clients.get(clientId);
122
+ if (!client) return "unknown";
123
+ const method = client.token_endpoint_auth_method;
124
+ const isConfidential = method === "client_secret_basic" || method === "client_secret_post";
125
+ if (!isConfidential) return "ok";
126
+ if (!client.client_secret) return "invalid";
127
+ if (!presentedSecret) return "invalid";
128
+ return timingSafeEqual(sha256(presentedSecret), sha256(client.client_secret)) ? "ok" : "invalid";
129
+ }
130
+ /** Whether a client id is known to the registry. */
131
+ has(clientId) {
132
+ return this.clients.has(clientId);
133
+ }
134
+ /**
135
+ * Add a dynamically-registered client (called by the register flow).
136
+ *
137
+ * @returns `true` if the client was registered, `false` if the dynamic-client
138
+ * capacity is already reached.
139
+ *
140
+ * SECURITY: `POST /oauth/register` may be unauthenticated (no
141
+ * `initialAccessToken`); without a bound an attacker can register unlimited
142
+ * clients and exhaust memory. Once the cap is reached we REJECT new dynamic
143
+ * registrations rather than evicting an existing one \u2014 this preserves every
144
+ * already-registered client (including confidential ones with a
145
+ * `client_secret`), which eviction would have silently invalidated. The
146
+ * register flow surfaces `false` as a `temporarily_unavailable` error.
147
+ * Pre-registered / declarative clients are exempt and never count toward the
148
+ * cap.
149
+ */
150
+ register(client) {
151
+ const cap = this.dynamicClientCap();
152
+ const dynamicCount = this.countDynamicClients();
153
+ const isNew = !this.clients.has(client.client_id);
154
+ if (isNew && dynamicCount >= cap) {
155
+ return false;
156
+ }
157
+ this.clients.set(client.client_id, client);
158
+ return true;
159
+ }
160
+ /** Configured dynamic-client cap, falling back to the default when invalid. */
161
+ dynamicClientCap() {
162
+ const configured = this.config.maxDynamicClients;
163
+ return Number.isSafeInteger(configured) && configured >= 0 ? configured : DEFAULT_MAX_DYNAMIC_CLIENTS;
164
+ }
165
+ countDynamicClients() {
166
+ let n = 0;
167
+ for (const c of this.clients.values()) if (c.dev && !c.preRegistered) n++;
168
+ return n;
169
+ }
170
+ /** Whether a redirect_uri allowlist is configured. */
171
+ hasRedirectAllowlist() {
172
+ return !!this.redirectGlobs && this.redirectGlobs.length > 0;
173
+ }
174
+ /**
175
+ * Whether `redirectUri` is permitted by the configured allowlist. Always
176
+ * `true` when no allowlist is configured (preserves default behavior).
177
+ */
178
+ isRedirectUriAllowed(redirectUri) {
179
+ if (!this.redirectGlobs || this.redirectGlobs.length === 0) {
180
+ return true;
181
+ }
182
+ return this.redirectGlobs.some((glob) => globMatchRedirectUri(glob, redirectUri));
183
+ }
184
+ /** Whether a client-id allowlist is configured. */
185
+ hasClientIdAllowlist() {
186
+ return !!this.config.allowedClientIds && this.config.allowedClientIds.length > 0;
187
+ }
188
+ /**
189
+ * Whether `clientId` is permitted by the configured client-id allowlist.
190
+ * Always `true` when no allowlist is configured (preserves default behavior).
191
+ */
192
+ isClientIdAllowed(clientId) {
193
+ const allow = this.config.allowedClientIds;
194
+ if (!allow || allow.length === 0) {
195
+ return true;
196
+ }
197
+ return allow.includes(clientId);
198
+ }
199
+ /** Whether an initial access token is required for DCR registration. */
200
+ requiresInitialAccessToken() {
201
+ return typeof this.config.initialAccessToken === "string" && this.config.initialAccessToken.length > 0;
202
+ }
203
+ /**
204
+ * Constant-time check of a presented bearer token against the configured
205
+ * initial access token. Returns `false` when none is configured (callers
206
+ * should gate on {@link requiresInitialAccessToken} first).
207
+ */
208
+ verifyInitialAccessToken(presented) {
209
+ const expected = this.config.initialAccessToken;
210
+ if (!expected) {
211
+ return false;
212
+ }
213
+ if (typeof presented !== "string" || presented.length === 0) {
214
+ return false;
215
+ }
216
+ return timingSafeEqual(sha256(presented), sha256(expected));
217
+ }
218
+ };
219
+ export {
220
+ DcrClientRegistry,
221
+ globMatch
222
+ };
@@ -0,0 +1,41 @@
1
+ // libs/auth/src/authorization/index.ts
2
+ import { AppAuthState } from "./authorization.types.mjs";
3
+ import {
4
+ authModeSchema,
5
+ authUserSchema,
6
+ authorizedToolSchema,
7
+ authorizedPromptSchema,
8
+ llmSafeAuthContextSchema,
9
+ appAuthStateSchema,
10
+ appAuthorizationRecordSchema,
11
+ progressiveAuthStateSchema
12
+ } from "./authorization.types.mjs";
13
+ import { AuthorizationBase } from "./authorization.class.mjs";
14
+ import { PublicAuthorization } from "./public.authorization.mjs";
15
+ import { TransparentAuthorization } from "./transparent.authorization.mjs";
16
+ import { OrchestratedAuthorization } from "./orchestrated.authorization.mjs";
17
+ import {
18
+ OrchestratedAuthAccessorAdapter,
19
+ NullOrchestratedAuthAccessor,
20
+ ORCHESTRATED_AUTH_ACCESSOR
21
+ } from "./orchestrated.accessor.mjs";
22
+ import { DcrClientRegistry } from "./dcr-client.registry.mjs";
23
+ export {
24
+ AppAuthState,
25
+ AuthorizationBase,
26
+ DcrClientRegistry,
27
+ NullOrchestratedAuthAccessor,
28
+ ORCHESTRATED_AUTH_ACCESSOR,
29
+ OrchestratedAuthAccessorAdapter,
30
+ OrchestratedAuthorization,
31
+ PublicAuthorization,
32
+ TransparentAuthorization,
33
+ appAuthStateSchema,
34
+ appAuthorizationRecordSchema,
35
+ authModeSchema,
36
+ authUserSchema,
37
+ authorizedPromptSchema,
38
+ authorizedToolSchema,
39
+ llmSafeAuthContextSchema,
40
+ progressiveAuthStateSchema
41
+ };
@@ -0,0 +1,86 @@
1
+ // libs/auth/src/authorization/orchestrated.accessor.ts
2
+ import { OrchestratedAuthNotAvailableError } from "../errors/auth-internal.errors.mjs";
3
+ var ORCHESTRATED_AUTH_ACCESSOR = /* @__PURE__ */ Symbol.for(
4
+ "frontmcp:ORCHESTRATED_AUTH_ACCESSOR"
5
+ );
6
+ var NullOrchestratedAuthAccessor = class {
7
+ primaryProviderId = void 0;
8
+ issuer = void 0;
9
+ authorizationId = "null";
10
+ isAuthenticated = false;
11
+ async getToken(providerId) {
12
+ throw new OrchestratedAuthNotAvailableError();
13
+ }
14
+ async tryGetToken(providerId) {
15
+ return null;
16
+ }
17
+ async getAppToken(appId) {
18
+ return null;
19
+ }
20
+ hasProvider(providerId) {
21
+ return false;
22
+ }
23
+ getProviderIds() {
24
+ return [];
25
+ }
26
+ isAppAuthorized(appId) {
27
+ return false;
28
+ }
29
+ getAllAuthorizedAppIds() {
30
+ return [];
31
+ }
32
+ getAppToolIds(appId) {
33
+ return void 0;
34
+ }
35
+ };
36
+ var OrchestratedAuthAccessorAdapter = class {
37
+ constructor(authorization) {
38
+ this.authorization = authorization;
39
+ }
40
+ authorization;
41
+ get primaryProviderId() {
42
+ return this.authorization.primaryProviderId;
43
+ }
44
+ get issuer() {
45
+ return this.authorization.issuer;
46
+ }
47
+ get authorizationId() {
48
+ return this.authorization.id;
49
+ }
50
+ get isAuthenticated() {
51
+ return !this.authorization.isAnonymous;
52
+ }
53
+ async getToken(providerId) {
54
+ return this.authorization.getToken(providerId);
55
+ }
56
+ async tryGetToken(providerId) {
57
+ try {
58
+ return await this.authorization.getToken(providerId);
59
+ } catch {
60
+ return null;
61
+ }
62
+ }
63
+ async getAppToken(appId) {
64
+ return this.authorization.getAppToken(appId);
65
+ }
66
+ hasProvider(providerId) {
67
+ return this.authorization.hasProvider(providerId);
68
+ }
69
+ getProviderIds() {
70
+ return this.authorization.getProviderIds();
71
+ }
72
+ isAppAuthorized(appId) {
73
+ return this.authorization.isAppAuthorized(appId);
74
+ }
75
+ getAllAuthorizedAppIds() {
76
+ return this.authorization.getAllAuthorizedAppIds();
77
+ }
78
+ getAppToolIds(appId) {
79
+ return this.authorization.getAppToolIds(appId);
80
+ }
81
+ };
82
+ export {
83
+ NullOrchestratedAuthAccessor,
84
+ ORCHESTRATED_AUTH_ACCESSOR,
85
+ OrchestratedAuthAccessorAdapter
86
+ };