@frontmcp/auth 1.10.16 → 1.11.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/browser/authorities/authorities.context.mjs +115 -0
- package/browser/authorities/authorities.engine.mjs +272 -0
- package/browser/authorities/authorities.errors.mjs +59 -0
- package/browser/authorities/authorities.evaluator.mjs +225 -0
- package/browser/authorities/authorities.metadata-augment.mjs +18 -0
- package/browser/authorities/authorities.registry.mjs +112 -0
- package/browser/authorities/authorities.schema.mjs +92 -0
- package/browser/authorities/authorities.scope-mapping.mjs +30 -0
- package/browser/authorities/authorities.validation.mjs +208 -0
- package/browser/authorities/index.mjs +43 -0
- package/browser/authorization/authorization.class.mjs +252 -0
- package/browser/authorization/authorization.types.mjs +64 -0
- package/browser/authorization/dcr-client.registry.mjs +222 -0
- package/browser/authorization/index.mjs +41 -0
- package/browser/authorization/orchestrated.accessor.mjs +86 -0
- package/browser/authorization/orchestrated.authorization.mjs +289 -0
- package/browser/authorization/public.authorization.mjs +124 -0
- package/browser/authorization/transparent.authorization.mjs +130 -0
- package/browser/cimd/cimd-redis.cache.mjs +231 -0
- package/browser/cimd/cimd.cache.mjs +215 -0
- package/browser/cimd/cimd.errors.mjs +142 -0
- package/browser/cimd/cimd.logger.mjs +15 -0
- package/browser/cimd/cimd.service.mjs +376 -0
- package/browser/cimd/cimd.types.mjs +228 -0
- package/browser/cimd/cimd.validator.mjs +217 -0
- package/browser/cimd/index.mjs +63 -0
- package/browser/common/auth-logger.interface.mjs +14 -0
- package/browser/common/index.mjs +6 -0
- package/browser/common/jwt.types.mjs +38 -0
- package/browser/common/session-user.types.mjs +0 -0
- package/browser/common/session.types.mjs +58 -0
- package/browser/common/zod-utils.mjs +0 -0
- package/browser/consent/consent.types.mjs +94 -0
- package/browser/consent/index.mjs +17 -0
- package/browser/context/frontmcp-auth-context.factory.mjs +29 -0
- package/browser/context/frontmcp-auth-context.impl.mjs +112 -0
- package/browser/context/index.mjs +7 -0
- package/browser/detection/auth-provider-detection.mjs +176 -0
- package/browser/detection/index.mjs +19 -0
- package/browser/errors/auth-internal.error.mjs +42 -0
- package/browser/errors/auth-internal.errors.mjs +150 -0
- package/browser/errors/index.mjs +53 -0
- package/browser/fetch/fetch-credential-middleware.mjs +91 -0
- package/browser/fetch/index.mjs +19 -0
- package/browser/index.mjs +573 -0
- package/browser/jwks/index.mjs +9 -0
- package/browser/jwks/jose.loader.mjs +28 -0
- package/browser/jwks/jwks.service.mjs +521 -0
- package/browser/jwks/jwks.utils.mjs +33 -0
- package/browser/options/app-auth.schema.mjs +18 -0
- package/browser/options/index.mjs +89 -0
- package/browser/options/orchestrated.schema.mjs +128 -0
- package/browser/options/public.schema.mjs +39 -0
- package/browser/options/schema.mjs +16 -0
- package/browser/options/shared.schemas.mjs +506 -0
- package/browser/options/static.schema.mjs +41 -0
- package/browser/options/transparent.schema.mjs +43 -0
- package/browser/options/utils.mjs +47 -0
- package/browser/people/index.mjs +15 -0
- package/browser/people/loopback-token.auth.mjs +115 -0
- package/browser/people/people-auth.options.mjs +37 -0
- package/browser/session/authorization-vault.mjs +279 -0
- package/browser/session/authorization.store.mjs +336 -0
- package/browser/session/consent.store.mjs +34 -0
- package/browser/session/credential-resume-link.mjs +43 -0
- package/browser/session/credentials-accessor.impl.mjs +54 -0
- package/browser/session/credentials-accessor.mjs +5 -0
- package/browser/session/encrypted-authorization-vault.mjs +405 -0
- package/browser/session/federated-auth.session.mjs +176 -0
- package/browser/session/incremental-auth-ticket.mjs +42 -0
- package/browser/session/index.mjs +266 -0
- package/browser/session/orchestrated-token.crypto.mjs +47 -0
- package/browser/session/orchestrated-token.store.mjs +286 -0
- package/browser/session/pending-login.mjs +37 -0
- package/browser/session/redis-session.store.mjs +255 -0
- package/browser/session/secure-store-accessor.impl.mjs +78 -0
- package/browser/session/secure-store-accessor.mjs +5 -0
- package/browser/session/secure-store-backends.mjs +111 -0
- package/browser/session/secure-store.factory.mjs +77 -0
- package/browser/session/session-credential-vault.factory.mjs +18 -0
- package/browser/session/session-credential-vault.mjs +243 -0
- package/browser/session/session-crypto.mjs +39 -0
- package/browser/session/session-rate-limiter.mjs +126 -0
- package/browser/session/session.transport.mjs +16 -0
- package/browser/session/signin-binding.mjs +30 -0
- package/browser/session/storage/in-memory-authorization-vault.mjs +28 -0
- package/browser/session/storage/index.mjs +14 -0
- package/browser/session/storage/storage-authorization-vault.mjs +301 -0
- package/browser/session/storage/storage-token-store.mjs +83 -0
- package/browser/session/storage-authorization.store.mjs +134 -0
- package/browser/session/storage-consent.store.mjs +33 -0
- package/browser/session/storage-federated-auth.session.mjs +55 -0
- package/browser/session/storage-orchestrated-token.store.mjs +197 -0
- package/browser/session/token-storage.factory.mjs +58 -0
- package/browser/session/token.refresh.mjs +36 -0
- package/browser/session/token.vault.mjs +61 -0
- package/browser/session/transport-session.types.mjs +108 -0
- package/browser/session/utils/auth-token.utils.mjs +58 -0
- package/browser/session/utils/index.mjs +23 -0
- package/browser/session/utils/session-crypto.utils.mjs +49 -0
- package/browser/session/utils/tiny-ttl-cache.mjs +46 -0
- package/browser/session/vault-encryption.mjs +187 -0
- package/browser/session/vercel-kv-session.store.mjs +230 -0
- package/browser/shared-server/index.mjs +17 -0
- package/browser/shared-server/shared-server-identity.mjs +55 -0
- package/browser/ui/base-layout.mjs +184 -0
- package/browser/ui/index.mjs +51 -0
- package/browser/ui/local-login.helper.mjs +46 -0
- package/browser/ui/templates.mjs +482 -0
- package/browser/utils/audience.validator.mjs +110 -0
- package/browser/utils/authorization-id.utils.mjs +10 -0
- package/browser/utils/index.mjs +45 -0
- package/browser/utils/redirect-uri.utils.mjs +22 -0
- package/browser/utils/scope-grant.utils.mjs +36 -0
- package/browser/utils/token-scopes.utils.mjs +12 -0
- package/browser/utils/www-authenticate.utils.mjs +164 -0
- package/browser/vault/auth-providers.accessor.impl.mjs +236 -0
- package/browser/vault/auth-providers.accessor.mjs +5 -0
- package/browser/vault/auth-providers.registry.mjs +140 -0
- package/browser/vault/auth-providers.types.mjs +47 -0
- package/browser/vault/auth-providers.vault.mjs +206 -0
- package/browser/vault/credential-cache.mjs +187 -0
- package/browser/vault/credential-helpers.mjs +15 -0
- package/browser/vault/credential-loaders/eager-loader.mjs +91 -0
- package/browser/vault/credential-loaders/lazy-loader.mjs +120 -0
- package/browser/vault/index.mjs +35 -0
- package/cimd/cimd.types.d.ts +1 -1
- package/cimd/cimd.types.d.ts.map +1 -1
- package/common/jwt.types.d.ts +1 -1
- package/common/jwt.types.d.ts.map +1 -1
- package/common/session.types.d.ts +9 -1
- package/common/session.types.d.ts.map +1 -1
- package/consent/consent.types.d.ts +1 -1
- package/consent/consent.types.d.ts.map +1 -1
- package/detection/auth-provider-detection.d.ts +1 -1
- package/detection/auth-provider-detection.d.ts.map +1 -1
- package/errors/auth-internal.errors.d.ts +6 -0
- package/errors/auth-internal.errors.d.ts.map +1 -1
- package/errors/index.d.ts +1 -1
- package/errors/index.d.ts.map +1 -1
- package/esm/index.mjs +2 -2
- package/esm/package.json +21 -5
- package/index.d.ts +5 -1
- package/index.d.ts.map +1 -1
- package/index.js +2 -2
- package/options/app-auth.schema.d.ts +19 -1
- package/options/app-auth.schema.d.ts.map +1 -1
- package/options/orchestrated.schema.d.ts +37 -1
- package/options/orchestrated.schema.d.ts.map +1 -1
- package/options/public.schema.d.ts +1 -1
- package/options/public.schema.d.ts.map +1 -1
- package/options/schema.d.ts +19 -1
- package/options/schema.d.ts.map +1 -1
- package/options/shared.schemas.d.ts +10 -14
- package/options/shared.schemas.d.ts.map +1 -1
- package/options/static.schema.d.ts +1 -1
- package/options/static.schema.d.ts.map +1 -1
- package/options/transparent.schema.d.ts +1 -1
- package/options/transparent.schema.d.ts.map +1 -1
- package/package.json +21 -5
- package/people/index.d.ts +3 -0
- package/people/index.d.ts.map +1 -0
- package/people/loopback-token.auth.d.ts +75 -0
- package/people/loopback-token.auth.d.ts.map +1 -0
- package/people/people-auth.options.d.ts +36 -0
- package/people/people-auth.options.d.ts.map +1 -0
- package/session/authorization-vault.d.ts +1 -1
- package/session/authorization-vault.d.ts.map +1 -1
- package/session/encrypted-authorization-vault.d.ts +1 -1
- package/session/encrypted-authorization-vault.d.ts.map +1 -1
- package/session/secure-store.factory.d.ts.map +1 -1
- package/shared-server/index.d.ts +3 -0
- package/shared-server/index.d.ts.map +1 -0
- package/shared-server/shared-server-identity.d.ts +26 -0
- 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
|
+
};
|