@cat-factory/integrations 0.130.2 → 0.131.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.
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/modules/audit/AuditService.d.ts +36 -0
- package/dist/modules/audit/AuditService.d.ts.map +1 -0
- package/dist/modules/audit/AuditService.js +39 -0
- package/dist/modules/audit/AuditService.js.map +1 -0
- package/dist/modules/mcpOAuth/McpOAuthService.d.ts +199 -0
- package/dist/modules/mcpOAuth/McpOAuthService.d.ts.map +1 -0
- package/dist/modules/mcpOAuth/McpOAuthService.js +489 -0
- package/dist/modules/mcpOAuth/McpOAuthService.js.map +1 -0
- package/dist/modules/mcpOAuth/mcpOAuthClient.d.ts +101 -0
- package/dist/modules/mcpOAuth/mcpOAuthClient.d.ts.map +1 -0
- package/dist/modules/mcpOAuth/mcpOAuthClient.js +389 -0
- package/dist/modules/mcpOAuth/mcpOAuthClient.js.map +1 -0
- package/package.json +4 -4
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
import { MCP_OAUTH_DEFAULT_HEADER, MCP_OAUTH_DEFAULT_HEADER_TEMPLATE, ValidationError, noopLogger, redactSecrets, } from '@cat-factory/kernel';
|
|
2
|
+
import { McpOAuthError, assertAllowedOAuthUrl, buildAuthorizationUrl, discoverMcpOAuthEndpoints, exchangeAuthorizationCode, refreshAccessToken, requestClientCredentialsToken, } from './mcpOAuthClient.js';
|
|
3
|
+
/**
|
|
4
|
+
* HKDF domain tag separating this store's ciphertexts from every other cipher in the platform.
|
|
5
|
+
* ONE tag covers both things sealed here (the stored token set and the in-flight authorization
|
|
6
|
+
* request), which is why each carries an explicit `kind` claim that the opener pins — the same
|
|
7
|
+
* discipline the token signer applies with its `aud`, so a value minted for one purpose can never
|
|
8
|
+
* be opened as the other.
|
|
9
|
+
*/
|
|
10
|
+
export const MCP_OAUTH_CIPHER_INFO = 'cat-factory:mcp-oauth';
|
|
11
|
+
/**
|
|
12
|
+
* How long an in-flight authorization request stays valid. Long enough to sign in to a vendor and
|
|
13
|
+
* approve a consent screen, short enough that an abandoned one is not a lasting artefact.
|
|
14
|
+
*/
|
|
15
|
+
const AUTHORIZATION_REQUEST_TTL_MS = 10 * 60 * 1000;
|
|
16
|
+
/**
|
|
17
|
+
* How far before its stated expiry an access token is treated as spent.
|
|
18
|
+
*
|
|
19
|
+
* A dispatched token is used by an agent in a container for the length of a run, not at the
|
|
20
|
+
* instant it is handed over, so "expires in 20 seconds" is functionally expired. Refreshing early
|
|
21
|
+
* costs one round trip; handing over a token that dies mid-run costs the agent its tool with no
|
|
22
|
+
* unavailability reason to state, because the platform believed it was wired.
|
|
23
|
+
*/
|
|
24
|
+
const EXPIRY_SKEW_MS = 60_000;
|
|
25
|
+
/** How many times a lost refresh race re-reads before giving up. */
|
|
26
|
+
const MAX_REFRESH_ATTEMPTS = 3;
|
|
27
|
+
/**
|
|
28
|
+
* Owns a workspace's OAuth grants against remote (`http`) MCP tool servers: starting a grant,
|
|
29
|
+
* completing one, minting and refreshing access tokens for a dispatch, and reporting what a board
|
|
30
|
+
* is connected to.
|
|
31
|
+
*
|
|
32
|
+
* The split from `CapabilityCredentialsService` beside it is the split between a credential a
|
|
33
|
+
* human TYPES and one a human GRANTS. A typed credential is inert until a dispatch reads it; a
|
|
34
|
+
* granted one expires, refreshes, gets revoked at the vendor, and belongs to a named person's
|
|
35
|
+
* account. That is why this is a service of its own rather than another key in the checklist: the
|
|
36
|
+
* checklist's whole shape (a key, a write-only value, a last-written date) cannot express any of it.
|
|
37
|
+
*
|
|
38
|
+
* STRICTLY the store plus the protocol. What a deployment DECLARES is registry state, and the
|
|
39
|
+
* caller passes the declaration in — this package has no business reaching into the agent-kind
|
|
40
|
+
* registry, exactly as the credential service has none.
|
|
41
|
+
*
|
|
42
|
+
* CONCURRENCY: the refresh path is the contended one, and it is contended by two dispatches rather
|
|
43
|
+
* than two humans. It rides a rev-guarded `compareAndSwap`, and the loser does NOT re-apply its own
|
|
44
|
+
* write (the rule every other rev-guarded path in this repo follows) — it re-reads and adopts the
|
|
45
|
+
* winner's tokens. Re-applying would be actively wrong here: an authorization server that rotates
|
|
46
|
+
* refresh tokens has already invalidated the loser's, so re-applying replaces a working grant with
|
|
47
|
+
* a dead one.
|
|
48
|
+
*/
|
|
49
|
+
export class McpOAuthService {
|
|
50
|
+
deps;
|
|
51
|
+
log;
|
|
52
|
+
constructor(deps) {
|
|
53
|
+
this.deps = deps;
|
|
54
|
+
this.log = deps.logger ?? noopLogger;
|
|
55
|
+
}
|
|
56
|
+
get fetchDeps() {
|
|
57
|
+
return this.deps.fetchImpl ? { fetch: this.deps.fetchImpl } : {};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Begin an `authorization_code` grant: resolve the endpoints, mint a PKCE pair, and return the
|
|
61
|
+
* vendor URL to send the operator to.
|
|
62
|
+
*
|
|
63
|
+
* Throws rather than returning a status, because every failure here is a REFUSAL an operator
|
|
64
|
+
* asked for directly (a declaration that cannot be granted, a deployment with no redirect URL,
|
|
65
|
+
* an authorization server that publishes nothing) and the surface that called it is a button
|
|
66
|
+
* press, not a run.
|
|
67
|
+
*/
|
|
68
|
+
async startAuthorization(input) {
|
|
69
|
+
if (input.oauth.grant !== 'authorization_code') {
|
|
70
|
+
throw new ValidationError(`Tool server '${input.serverId}' authenticates with the client-credentials grant, which ` +
|
|
71
|
+
`needs no authorization: its token is minted from the deployment's own client on the ` +
|
|
72
|
+
`first dispatch that needs one.`, { reason: 'oauth_grant_not_interactive' });
|
|
73
|
+
}
|
|
74
|
+
const endpoints = await this.resolveEndpoints(input.serverUrl, input.oauth);
|
|
75
|
+
const codeVerifier = randomCodeVerifier();
|
|
76
|
+
const request = {
|
|
77
|
+
kind: 'authorization-request',
|
|
78
|
+
workspaceId: input.workspaceId,
|
|
79
|
+
serverId: input.serverId,
|
|
80
|
+
userId: input.userId,
|
|
81
|
+
codeVerifier,
|
|
82
|
+
redirectUri: input.redirectUri,
|
|
83
|
+
tokenUrl: endpoints.tokenUrl,
|
|
84
|
+
useBasicAuth: endpoints.useBasicAuth,
|
|
85
|
+
resource: input.oauth.resource ?? input.serverUrl,
|
|
86
|
+
exp: this.deps.clock.now() + AUTHORIZATION_REQUEST_TTL_MS,
|
|
87
|
+
};
|
|
88
|
+
return {
|
|
89
|
+
url: buildAuthorizationUrl({
|
|
90
|
+
authorizationUrl: endpoints.authorizationUrl,
|
|
91
|
+
clientId: input.oauth.clientId,
|
|
92
|
+
redirectUri: input.redirectUri,
|
|
93
|
+
state: await this.deps.secretCipher.encrypt(JSON.stringify(request)),
|
|
94
|
+
codeChallenge: await codeChallengeFor(codeVerifier),
|
|
95
|
+
...(input.oauth.scopes?.length ? { scopes: input.oauth.scopes } : {}),
|
|
96
|
+
resource: request.resource,
|
|
97
|
+
}),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Open the sealed `state` a callback carried, or null when it is absent, forged, expired or not
|
|
102
|
+
* an authorization request at all.
|
|
103
|
+
*
|
|
104
|
+
* Null rather than a thrown cause on purpose: the four are indistinguishable to the caller and
|
|
105
|
+
* every one of them is a 401, so telling them apart would only tell an attacker which of their
|
|
106
|
+
* guesses was closer.
|
|
107
|
+
*/
|
|
108
|
+
async readAuthorizationRequest(state) {
|
|
109
|
+
if (!state)
|
|
110
|
+
return null;
|
|
111
|
+
let parsed;
|
|
112
|
+
try {
|
|
113
|
+
parsed = JSON.parse(await this.deps.secretCipher.decrypt(state));
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// silent-catch-ok: a state that will not open is a 401 whatever the reason, and the reason is
|
|
117
|
+
// attacker-supplied. The caller logs the refusal with the request's own correlation id.
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const request = parsed;
|
|
121
|
+
if (!request || request.kind !== 'authorization-request')
|
|
122
|
+
return null;
|
|
123
|
+
if (typeof request.exp !== 'number' || request.exp < this.deps.clock.now())
|
|
124
|
+
return null;
|
|
125
|
+
return request;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Complete a grant: exchange the code for tokens and seal them for the workspace.
|
|
129
|
+
*
|
|
130
|
+
* A BLIND upsert, unlike the refresh path: a human just authorised, and what they authorised
|
|
131
|
+
* supersedes whatever the row held — including a grant a colleague made a minute earlier, which
|
|
132
|
+
* is exactly the "reconnect with a different account" case a rev guard would turn into a
|
|
133
|
+
* spurious conflict.
|
|
134
|
+
*/
|
|
135
|
+
async completeAuthorization(request, input) {
|
|
136
|
+
const tokens = await exchangeAuthorizationCode({
|
|
137
|
+
tokenUrl: request.tokenUrl,
|
|
138
|
+
clientId: input.clientId,
|
|
139
|
+
...(input.clientSecret ? { clientSecret: input.clientSecret } : {}),
|
|
140
|
+
useBasicAuth: request.useBasicAuth,
|
|
141
|
+
resource: request.resource,
|
|
142
|
+
code: input.code,
|
|
143
|
+
redirectUri: request.redirectUri,
|
|
144
|
+
codeVerifier: request.codeVerifier,
|
|
145
|
+
}, this.fetchDeps).catch((error) => {
|
|
146
|
+
throw asRefusal(error);
|
|
147
|
+
});
|
|
148
|
+
const now = this.deps.clock.now();
|
|
149
|
+
await this.deps.mcpOAuthGrantRepository.upsert(await this.buildRecord({
|
|
150
|
+
workspaceId: request.workspaceId,
|
|
151
|
+
serverId: request.serverId,
|
|
152
|
+
tokens,
|
|
153
|
+
now,
|
|
154
|
+
rev: 0,
|
|
155
|
+
createdAt: now,
|
|
156
|
+
summary: {
|
|
157
|
+
connectedAt: now,
|
|
158
|
+
...(request.userId ? { connectedBy: request.userId } : {}),
|
|
159
|
+
},
|
|
160
|
+
}));
|
|
161
|
+
this.log.info('mcp tool server oauth grant stored', {
|
|
162
|
+
workspaceId: request.workspaceId,
|
|
163
|
+
toolServerId: request.serverId,
|
|
164
|
+
refreshable: Boolean(tokens.refreshToken),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
/** Drop a workspace's grant for one server. Idempotent: a missing row is already disconnected. */
|
|
168
|
+
async disconnect(workspaceId, serverId) {
|
|
169
|
+
await this.deps.mcpOAuthGrantRepository.delete(workspaceId, serverId);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* The non-secret connection state of every grant a workspace holds, keyed by server id.
|
|
173
|
+
*
|
|
174
|
+
* ONE read for the whole inventory rather than a lookup per declared server: the operator surface
|
|
175
|
+
* renders a row per DECLARATION, and the declarations are the deployment's whole registry.
|
|
176
|
+
*/
|
|
177
|
+
async listStatuses(workspaceId) {
|
|
178
|
+
const records = await this.deps.mcpOAuthGrantRepository.listByWorkspace(workspaceId);
|
|
179
|
+
return new Map(records.map((record) => [record.serverId, parseSummary(record)]));
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* The access token one dispatch needs, refreshing or minting it when what is stored will not do.
|
|
183
|
+
*
|
|
184
|
+
* Never throws: a dispatch asking for a tool is not a place to fail a run, so every failure comes
|
|
185
|
+
* back as a `token_failed` result the caller states to the agent. That is the same disposition
|
|
186
|
+
* `resolveToolServers` gives an unresolved static credential, and for the same reason.
|
|
187
|
+
*/
|
|
188
|
+
async accessToken(input) {
|
|
189
|
+
try {
|
|
190
|
+
const token = await this.resolveToken(input);
|
|
191
|
+
return token
|
|
192
|
+
? {
|
|
193
|
+
status: 'ok',
|
|
194
|
+
header: input.oauth.header ?? MCP_OAUTH_DEFAULT_HEADER,
|
|
195
|
+
value: (input.oauth.headerTemplate ?? MCP_OAUTH_DEFAULT_HEADER_TEMPLATE).replaceAll('{value}', token),
|
|
196
|
+
}
|
|
197
|
+
: { status: 'not_connected' };
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
const message = describeOAuthError(error);
|
|
201
|
+
this.log.warn('mcp tool server oauth token could not be obtained', {
|
|
202
|
+
workspaceId: input.workspaceId,
|
|
203
|
+
toolServerId: input.serverId,
|
|
204
|
+
detail: message,
|
|
205
|
+
});
|
|
206
|
+
await this.recordFailure(input.workspaceId, input.serverId, message);
|
|
207
|
+
return { status: 'token_failed', error: message };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/** The raw access token, or null when nothing is granted. Throws on a real failure. */
|
|
211
|
+
async resolveToken(input) {
|
|
212
|
+
for (let attempt = 0; attempt < MAX_REFRESH_ATTEMPTS; attempt++) {
|
|
213
|
+
const record = await this.deps.mcpOAuthGrantRepository.get(input.workspaceId, input.serverId);
|
|
214
|
+
const stored = record ? await this.openTokens(record) : null;
|
|
215
|
+
if (stored && !this.isSpent(stored)) {
|
|
216
|
+
// A live token also settles any failure the summary still claims: `lastError` is written by
|
|
217
|
+
// whichever dispatch could not mint one, and nothing else would ever take it back off a
|
|
218
|
+
// connection that started working again. Only touches the row when there is something to
|
|
219
|
+
// clear, so the ordinary dispatch stays a single read.
|
|
220
|
+
if (record && parseSummary(record).lastError !== undefined)
|
|
221
|
+
await this.clearFailure(record);
|
|
222
|
+
return stored.accessToken;
|
|
223
|
+
}
|
|
224
|
+
if (input.oauth.grant === 'authorization_code' && !stored)
|
|
225
|
+
return null;
|
|
226
|
+
if (input.oauth.grant === 'authorization_code' && !stored?.refreshToken) {
|
|
227
|
+
throw new McpOAuthError(`The stored access token has expired and the authorization server issued no refresh ` +
|
|
228
|
+
`token, so the connection has to be granted again.`, true);
|
|
229
|
+
}
|
|
230
|
+
let tokens;
|
|
231
|
+
try {
|
|
232
|
+
tokens = await this.mintTokens(input, stored?.refreshToken);
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
// The refresh race the rev guard below CANNOT settle, and the likelier half of it against a
|
|
236
|
+
// rotating authorization server. Two dispatches find the same token spent and POST the same
|
|
237
|
+
// refresh token; the winner's exchange rotates it, which INVALIDATES the loser's copy, so
|
|
238
|
+
// the loser fails right here with `invalid_grant` and never reaches the compareAndSwap that
|
|
239
|
+
// would have told it it lost. Re-reading is what tells the two apart: a row that has moved
|
|
240
|
+
// on to a live token means a peer succeeded, and this dispatch wants a token rather than a
|
|
241
|
+
// diagnosis. With nothing new stored, the failure is real and propagates untouched.
|
|
242
|
+
const adopted = await this.adoptConcurrentToken(input.workspaceId, input.serverId, record);
|
|
243
|
+
if (adopted) {
|
|
244
|
+
this.log.info('adopted a concurrently refreshed mcp oauth token after a lost race', {
|
|
245
|
+
workspaceId: input.workspaceId,
|
|
246
|
+
toolServerId: input.serverId,
|
|
247
|
+
});
|
|
248
|
+
return adopted;
|
|
249
|
+
}
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
const now = this.deps.clock.now();
|
|
253
|
+
const swapped = await this.deps.mcpOAuthGrantRepository.compareAndSwap(await this.buildRecord({
|
|
254
|
+
workspaceId: input.workspaceId,
|
|
255
|
+
serverId: input.serverId,
|
|
256
|
+
tokens: {
|
|
257
|
+
...tokens,
|
|
258
|
+
// An authorization server that rotates refresh tokens returns a new one; one that does
|
|
259
|
+
// not returns none, and DROPPING the old one there would turn a working grant into a
|
|
260
|
+
// single-use one. Carrying it forward is what makes both behaviours refreshable.
|
|
261
|
+
...(tokens.refreshToken ? {} : { refreshToken: stored?.refreshToken }),
|
|
262
|
+
},
|
|
263
|
+
now,
|
|
264
|
+
rev: (record?.rev ?? -1) + 1,
|
|
265
|
+
createdAt: record?.createdAt ?? now,
|
|
266
|
+
summary: {
|
|
267
|
+
...(record ? dropError(parseSummary(record)) : {}),
|
|
268
|
+
...(record ? {} : { connectedAt: now }),
|
|
269
|
+
},
|
|
270
|
+
}), record?.rev ?? null);
|
|
271
|
+
// Lost the race: another dispatch refreshed first. Loop back and read THEIR tokens rather
|
|
272
|
+
// than re-applying ours — a rotated refresh token makes ours the stale set, so re-applying
|
|
273
|
+
// would replace a working grant with a dead one.
|
|
274
|
+
if (swapped)
|
|
275
|
+
return tokens.accessToken;
|
|
276
|
+
}
|
|
277
|
+
throw new McpOAuthError(`The stored grant is being refreshed by several runs at once and this dispatch could not ` +
|
|
278
|
+
`settle on a token; retry.`, false);
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* One exchange at the token endpoint: the machine grant mints from the deployment's own client,
|
|
282
|
+
* the interactive one spends the stored refresh token.
|
|
283
|
+
*
|
|
284
|
+
* Split out of {@link resolveToken} so that method reads as what it is (the read, the spend
|
|
285
|
+
* check, the race handling and the swap) rather than carrying the wire shape of two requests
|
|
286
|
+
* through the middle of it.
|
|
287
|
+
*/
|
|
288
|
+
async mintTokens(input, refreshToken) {
|
|
289
|
+
const endpoints = await this.resolveEndpoints(input.serverUrl, input.oauth);
|
|
290
|
+
const common = {
|
|
291
|
+
tokenUrl: endpoints.tokenUrl,
|
|
292
|
+
clientId: input.oauth.clientId,
|
|
293
|
+
...(input.clientSecret ? { clientSecret: input.clientSecret } : {}),
|
|
294
|
+
useBasicAuth: endpoints.useBasicAuth,
|
|
295
|
+
resource: input.oauth.resource ?? input.serverUrl,
|
|
296
|
+
};
|
|
297
|
+
const scopes = input.oauth.scopes?.length ? { scopes: input.oauth.scopes } : {};
|
|
298
|
+
return input.oauth.grant === 'client_credentials'
|
|
299
|
+
? requestClientCredentialsToken({ ...common, ...scopes }, this.fetchDeps)
|
|
300
|
+
: // Reached only past the guards above, which return or throw when there is no refresh token.
|
|
301
|
+
refreshAccessToken({ ...common, refreshToken: refreshToken }, this.fetchDeps);
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* The token a CONCURRENT dispatch stored while this one was failing to mint its own, or null.
|
|
305
|
+
*
|
|
306
|
+
* Best effort by construction: it runs on a path that already has a real error to report, so
|
|
307
|
+
* anything that goes wrong here (an unreadable row, a rotated key) means only that there is
|
|
308
|
+
* nothing to adopt, and the caller rethrows the failure that brought it here. Requires the row to
|
|
309
|
+
* have MOVED: an unchanged `rev` is the same grant this attempt already read and failed with,
|
|
310
|
+
* and returning its expired token would hand the run a credential the vendor has finished with.
|
|
311
|
+
*/
|
|
312
|
+
async adoptConcurrentToken(workspaceId, serverId, seen) {
|
|
313
|
+
try {
|
|
314
|
+
const record = await this.deps.mcpOAuthGrantRepository.get(workspaceId, serverId);
|
|
315
|
+
if (!record || record.rev === seen?.rev)
|
|
316
|
+
return null;
|
|
317
|
+
const stored = await this.openTokens(record);
|
|
318
|
+
return stored && !this.isSpent(stored) ? stored.accessToken : null;
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
// silent-catch-ok: this is a recovery read behind an error that is about to propagate, and
|
|
322
|
+
// its own failure is not a second thing to report: it only means nothing could be adopted.
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/** Endpoints: the declaration's, when it pinned them, else discovered from the server url. */
|
|
327
|
+
async resolveEndpoints(serverUrl, oauth) {
|
|
328
|
+
if (oauth.authorizationUrl && oauth.tokenUrl) {
|
|
329
|
+
// The SAME floor a discovered endpoint and every redirect hop is held to, from the one
|
|
330
|
+
// implementation: a rule enforced on two of three paths is not a rule.
|
|
331
|
+
assertAllowedOAuthUrl(oauth.authorizationUrl, 'declared OAuth authorizationUrl');
|
|
332
|
+
assertAllowedOAuthUrl(oauth.tokenUrl, 'declared OAuth tokenUrl');
|
|
333
|
+
return {
|
|
334
|
+
authorizationUrl: oauth.authorizationUrl,
|
|
335
|
+
tokenUrl: oauth.tokenUrl,
|
|
336
|
+
useBasicAuth: false,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
const discovered = await discoverMcpOAuthEndpoints(serverUrl, this.fetchDeps);
|
|
340
|
+
// A HALF-declared pair still overrides its half: pinning one endpoint and discovering the other
|
|
341
|
+
// is a legitimate declaration (a vendor whose metadata is right about one and stale about the
|
|
342
|
+
// other), and silently ignoring the pin would send tokens somewhere the operator refused.
|
|
343
|
+
return {
|
|
344
|
+
authorizationUrl: oauth.authorizationUrl ?? discovered.authorizationUrl,
|
|
345
|
+
tokenUrl: oauth.tokenUrl ?? discovered.tokenUrl,
|
|
346
|
+
useBasicAuth: discovered.useBasicAuth,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
/** Whether a stored access token is too close to its expiry to hand to a run. */
|
|
350
|
+
isSpent(tokens) {
|
|
351
|
+
return (tokens.expiresAt !== undefined && tokens.expiresAt - EXPIRY_SKEW_MS <= this.deps.clock.now());
|
|
352
|
+
}
|
|
353
|
+
async openTokens(record) {
|
|
354
|
+
try {
|
|
355
|
+
const parsed = JSON.parse(await this.deps.secretCipher.decrypt(record.tokens));
|
|
356
|
+
return parsed?.kind === 'tokens' && typeof parsed.accessToken === 'string' ? parsed : null;
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
// A row that will not open is a rotated ENCRYPTION_KEY or a corrupt blob, and it is
|
|
360
|
+
// permanent: nothing this process does will decrypt it, so it must read as a broken
|
|
361
|
+
// connection to be re-granted rather than as an absent one that a dispatch quietly retries.
|
|
362
|
+
throw new McpOAuthError(`The stored grant could not be opened (${describeOAuthError(error)}). It was sealed with a ` +
|
|
363
|
+
`different encryption key; disconnect and grant it again.`, true);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
async buildRecord(input) {
|
|
367
|
+
const expiresAt = input.tokens.expiresIn !== undefined ? input.now + input.tokens.expiresIn * 1000 : undefined;
|
|
368
|
+
const stored = {
|
|
369
|
+
kind: 'tokens',
|
|
370
|
+
accessToken: input.tokens.accessToken,
|
|
371
|
+
...(input.tokens.refreshToken ? { refreshToken: input.tokens.refreshToken } : {}),
|
|
372
|
+
...(expiresAt !== undefined ? { expiresAt } : {}),
|
|
373
|
+
...(input.tokens.scope ? { scope: input.tokens.scope } : {}),
|
|
374
|
+
};
|
|
375
|
+
const summary = {
|
|
376
|
+
...input.summary,
|
|
377
|
+
...(input.tokens.scope ? { scopes: input.tokens.scope.split(/\s+/).filter(Boolean) } : {}),
|
|
378
|
+
...(expiresAt !== undefined ? { expiresAt } : {}),
|
|
379
|
+
refreshable: Boolean(stored.refreshToken),
|
|
380
|
+
};
|
|
381
|
+
return {
|
|
382
|
+
workspaceId: input.workspaceId,
|
|
383
|
+
serverId: input.serverId,
|
|
384
|
+
tokens: await this.deps.secretCipher.encrypt(JSON.stringify(stored)),
|
|
385
|
+
summary: JSON.stringify(summary),
|
|
386
|
+
rev: input.rev,
|
|
387
|
+
createdAt: input.createdAt,
|
|
388
|
+
updatedAt: input.now,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Record on the SUMMARY that the last token exchange failed, so the operator surface can say a
|
|
393
|
+
* connection stopped working without anyone reading a run's prompt.
|
|
394
|
+
*
|
|
395
|
+
* Best effort by construction, and it only ever touches a row that already exists: a failure with
|
|
396
|
+
* nothing stored is `not_connected`, which the surface already states. The rev guard is NOT
|
|
397
|
+
* retried on a lost swap: the note is advisory, and losing it to a concurrent refresh that
|
|
398
|
+
* SUCCEEDED is the correct outcome rather than a lost write.
|
|
399
|
+
*/
|
|
400
|
+
async recordFailure(workspaceId, serverId, message) {
|
|
401
|
+
const record = await this.deps.mcpOAuthGrantRepository.get(workspaceId, serverId);
|
|
402
|
+
if (!record)
|
|
403
|
+
return;
|
|
404
|
+
await this.writeSummary(record, { ...parseSummary(record), lastError: message });
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Take a recorded failure back off a connection that has started working again.
|
|
408
|
+
*
|
|
409
|
+
* The other half of {@link recordFailure}, and the surface reads wrong without it: `lastError`
|
|
410
|
+
* describes the last exchange that failed, so on a token that is merely CACHED (the common
|
|
411
|
+
* dispatch, which mints nothing) nothing would ever clear it, and one transient vendor outage
|
|
412
|
+
* would leave a red "the last token renewal failed" banner on a working grant until the access
|
|
413
|
+
* token happened to expire. Same advisory, same unretried rev guard.
|
|
414
|
+
*/
|
|
415
|
+
async clearFailure(record) {
|
|
416
|
+
await this.writeSummary(record, dropError(parseSummary(record)));
|
|
417
|
+
}
|
|
418
|
+
/** One rev-guarded, unretried write of the non-secret half. */
|
|
419
|
+
async writeSummary(record, summary) {
|
|
420
|
+
await this.deps.mcpOAuthGrantRepository.compareAndSwap({
|
|
421
|
+
...record,
|
|
422
|
+
summary: JSON.stringify(summary),
|
|
423
|
+
rev: record.rev + 1,
|
|
424
|
+
updatedAt: this.deps.clock.now(),
|
|
425
|
+
}, record.rev);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
/** Parse the persisted non-secret summary, tolerating a corrupt row (the view still loads). */
|
|
429
|
+
function parseSummary(record) {
|
|
430
|
+
try {
|
|
431
|
+
const parsed = JSON.parse(record.summary);
|
|
432
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
433
|
+
}
|
|
434
|
+
catch {
|
|
435
|
+
// silent-catch-ok: a summary is a display projection of a row whose SEALED half is the truth,
|
|
436
|
+
// so a drifted one must never keep an operator from seeing that the grant exists.
|
|
437
|
+
return {};
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
/** The summary with any recorded failure cleared — what a successful exchange leaves behind. */
|
|
441
|
+
function dropError(summary) {
|
|
442
|
+
const { lastError: _dropped, ...rest } = summary;
|
|
443
|
+
return rest;
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* An OAuth failure as an operator-facing sentence, scrubbed.
|
|
447
|
+
*
|
|
448
|
+
* `describeError` in kernel would do the scrubbing, and this exists beside it because an
|
|
449
|
+
* `McpOAuthError`'s message IS the operator-facing sentence: wrapping it in a generic description
|
|
450
|
+
* would bury the one part of it that names the fix.
|
|
451
|
+
*/
|
|
452
|
+
function describeOAuthError(error) {
|
|
453
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
454
|
+
return redactSecrets(raw) ?? 'unknown error';
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* An OAuth failure as an HTTP refusal, for the INTERACTIVE paths (starting and completing a grant),
|
|
458
|
+
* where the caller is a person and a thrown cause is the answer they asked for.
|
|
459
|
+
*
|
|
460
|
+
* A PERMANENT failure becomes a 422 carrying `details.reason`, because what must change is the
|
|
461
|
+
* declaration or the grant and the operator is the one who can change it. A TRANSIENT one is
|
|
462
|
+
* rethrown untouched and surfaces as a 500, which is the honest status for "the vendor's
|
|
463
|
+
* authorization server did not answer": mapping it to a 4xx would tell the operator to fix
|
|
464
|
+
* something that is not theirs.
|
|
465
|
+
*/
|
|
466
|
+
function asRefusal(error) {
|
|
467
|
+
if (error instanceof McpOAuthError && error.permanent) {
|
|
468
|
+
return new ValidationError(describeOAuthError(error), { reason: 'oauth_exchange_refused' });
|
|
469
|
+
}
|
|
470
|
+
return error;
|
|
471
|
+
}
|
|
472
|
+
/** A PKCE code verifier: 32 random bytes, base64url — the RFC 7636 recommended shape. */
|
|
473
|
+
function randomCodeVerifier() {
|
|
474
|
+
const bytes = new Uint8Array(32);
|
|
475
|
+
crypto.getRandomValues(bytes);
|
|
476
|
+
return base64url(bytes);
|
|
477
|
+
}
|
|
478
|
+
/** The S256 challenge for a verifier. */
|
|
479
|
+
async function codeChallengeFor(verifier) {
|
|
480
|
+
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
|
|
481
|
+
return base64url(new Uint8Array(digest));
|
|
482
|
+
}
|
|
483
|
+
function base64url(bytes) {
|
|
484
|
+
let binary = '';
|
|
485
|
+
for (const byte of bytes)
|
|
486
|
+
binary += String.fromCharCode(byte);
|
|
487
|
+
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
488
|
+
}
|
|
489
|
+
//# sourceMappingURL=McpOAuthService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"McpOAuthService.js","sourceRoot":"","sources":["../../../src/modules/mcpOAuth/McpOAuthService.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/** How long the whole discovery walk, or one token call, may take. */
|
|
2
|
+
export declare const MCP_OAUTH_TIMEOUT_MS = 10000;
|
|
3
|
+
/**
|
|
4
|
+
* The one URL floor every request in this module is held to, applied to DECLARED endpoints,
|
|
5
|
+
* DISCOVERED ones, and every redirect hop alike.
|
|
6
|
+
*
|
|
7
|
+
* Two rules, and the second is here because discovery reads a THIRD PARTY's document. A remote
|
|
8
|
+
* server's protected-resource metadata names its authorization servers, so a compromised or hostile
|
|
9
|
+
* server chooses URLs this deployment then fetches; `isAllowedMcpHttpUrl` alone would accept
|
|
10
|
+
* `https://169.254.169.254/…` and turn the walk into a probe of the instance-metadata service.
|
|
11
|
+
* Blocking metadata hosts while still ALLOWING private and loopback ones is the same trade the
|
|
12
|
+
* Kubernetes apiserver guard makes, and it keeps a sidecar MCP server on a private host connectable.
|
|
13
|
+
*
|
|
14
|
+
* One floor for every URL rather than a stricter rule for discovered ones: a declaration pointing an
|
|
15
|
+
* OAuth endpoint at a metadata host is nonsense too, so there is nothing to gain by admitting it.
|
|
16
|
+
*/
|
|
17
|
+
export declare function assertAllowedOAuthUrl(raw: string, what: string): void;
|
|
18
|
+
/** Injected so tests drive the whole flow without a network. */
|
|
19
|
+
export interface McpOAuthFetch {
|
|
20
|
+
fetch?: typeof fetch;
|
|
21
|
+
}
|
|
22
|
+
/** Where a server's authorization server is, and how it wants the client to authenticate. */
|
|
23
|
+
export interface McpOAuthEndpoints {
|
|
24
|
+
authorizationUrl: string;
|
|
25
|
+
tokenUrl: string;
|
|
26
|
+
/**
|
|
27
|
+
* Whether the token endpoint is asked for HTTP Basic client authentication instead of the
|
|
28
|
+
* credentials-in-the-body form.
|
|
29
|
+
*
|
|
30
|
+
* Defaults to the body form (`client_secret_post`), which every authorization server this
|
|
31
|
+
* platform has met accepts, and flips only when the AS metadata advertises Basic and NOT post.
|
|
32
|
+
* Deciding it from the metadata rather than trying both matters because a retry-on-401 would
|
|
33
|
+
* send the client secret twice, to a server that has already refused it once.
|
|
34
|
+
*/
|
|
35
|
+
useBasicAuth: boolean;
|
|
36
|
+
}
|
|
37
|
+
/** A token response, normalised. `expiresIn` is seconds, as the wire states it. */
|
|
38
|
+
export interface McpOAuthTokens {
|
|
39
|
+
accessToken: string;
|
|
40
|
+
refreshToken?: string;
|
|
41
|
+
expiresIn?: number;
|
|
42
|
+
scope?: string;
|
|
43
|
+
tokenType?: string;
|
|
44
|
+
}
|
|
45
|
+
/** A failure with a cause an operator can act on. Never carries a credential (see `describe`). */
|
|
46
|
+
export declare class McpOAuthError extends Error {
|
|
47
|
+
/** True when retrying cannot help: the grant or the client registration must change. */
|
|
48
|
+
readonly permanent: boolean;
|
|
49
|
+
constructor(message: string,
|
|
50
|
+
/** True when retrying cannot help: the grant or the client registration must change. */
|
|
51
|
+
permanent: boolean);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Discover a remote MCP server's authorization endpoints, per the MCP authorization spec: the
|
|
55
|
+
* server's own protected-resource metadata (RFC 9728) names its authorization server, and that
|
|
56
|
+
* server's metadata (RFC 8414, or OpenID Connect discovery) names the endpoints.
|
|
57
|
+
*
|
|
58
|
+
* The walk is what makes a vendor server connectable from a declaration that names only its url.
|
|
59
|
+
* Without it a deployment has to find two endpoint URLs in a vendor's docs, and they are exactly
|
|
60
|
+
* the strings a vendor changes when it re-platforms.
|
|
61
|
+
*
|
|
62
|
+
* EVERY url the walk touches goes through {@link assertAllowedOAuthUrl}: each candidate, each
|
|
63
|
+
* redirect hop, and both endpoints that come out of it. A metadata document is a third party
|
|
64
|
+
* telling this deployment where to send its client secret and receive its tokens, which is the one
|
|
65
|
+
* place in this flow where an outsider chooses a URL this side then fetches, so it is also the one
|
|
66
|
+
* place where checking the first URL and trusting the rest would not be checking at all.
|
|
67
|
+
*/
|
|
68
|
+
export declare function discoverMcpOAuthEndpoints(serverUrl: string, deps?: McpOAuthFetch): Promise<McpOAuthEndpoints>;
|
|
69
|
+
/** The authorization URL an operator's browser is sent to. */
|
|
70
|
+
export declare function buildAuthorizationUrl(input: {
|
|
71
|
+
authorizationUrl: string;
|
|
72
|
+
clientId: string;
|
|
73
|
+
redirectUri: string;
|
|
74
|
+
state: string;
|
|
75
|
+
codeChallenge: string;
|
|
76
|
+
scopes?: string[];
|
|
77
|
+
resource: string;
|
|
78
|
+
}): string;
|
|
79
|
+
export interface TokenRequestInput {
|
|
80
|
+
tokenUrl: string;
|
|
81
|
+
clientId: string;
|
|
82
|
+
clientSecret?: string;
|
|
83
|
+
useBasicAuth?: boolean;
|
|
84
|
+
resource: string;
|
|
85
|
+
}
|
|
86
|
+
/** Exchange an authorization code (plus its PKCE verifier) for a token set. */
|
|
87
|
+
export declare function exchangeAuthorizationCode(input: TokenRequestInput & {
|
|
88
|
+
code: string;
|
|
89
|
+
redirectUri: string;
|
|
90
|
+
codeVerifier: string;
|
|
91
|
+
}, deps?: McpOAuthFetch): Promise<McpOAuthTokens>;
|
|
92
|
+
/** Exchange a refresh token for a fresh token set. */
|
|
93
|
+
export declare function refreshAccessToken(input: TokenRequestInput & {
|
|
94
|
+
refreshToken: string;
|
|
95
|
+
scopes?: string[];
|
|
96
|
+
}, deps?: McpOAuthFetch): Promise<McpOAuthTokens>;
|
|
97
|
+
/** Mint a token from the deployment's own client credentials — the no-human grant. */
|
|
98
|
+
export declare function requestClientCredentialsToken(input: TokenRequestInput & {
|
|
99
|
+
scopes?: string[];
|
|
100
|
+
}, deps?: McpOAuthFetch): Promise<McpOAuthTokens>;
|
|
101
|
+
//# sourceMappingURL=mcpOAuthClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcpOAuthClient.d.ts","sourceRoot":"","sources":["../../../src/modules/mcpOAuth/mcpOAuthClient.ts"],"names":[],"mappings":"AAkBA,sEAAsE;AACtE,eAAO,MAAM,oBAAoB,QAAS,CAAA;AAa1C;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAwBrE;AAED,gEAAgE;AAChE,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,OAAO,KAAK,CAAA;CACrB;AAED,6FAA6F;AAC7F,MAAM,WAAW,iBAAiB;IAChC,gBAAgB,EAAE,MAAM,CAAA;IACxB,QAAQ,EAAE,MAAM,CAAA;IAChB;;;;;;;;OAQG;IACH,YAAY,EAAE,OAAO,CAAA;CACtB;AAED,mFAAmF;AACnF,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,kGAAkG;AAClG,qBAAa,aAAc,SAAQ,KAAK;IAGpC,wFAAwF;IACxF,QAAQ,CAAC,SAAS,EAAE,OAAO;IAH7B,YACE,OAAO,EAAE,MAAM;IACf,wFAAwF;IAC/E,SAAS,EAAE,OAAO,EAI5B;CACF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,yBAAyB,CAC7C,SAAS,EAAE,MAAM,EACjB,IAAI,GAAE,aAAkB,GACvB,OAAO,CAAC,iBAAiB,CAAC,CA2D5B;AA6ED,8DAA8D;AAC9D,wBAAgB,qBAAqB,CAAC,KAAK,EAAE;IAC3C,gBAAgB,EAAE,MAAM,CAAA;IACxB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,KAAK,EAAE,MAAM,CAAA;IACb,aAAa,EAAE,MAAM,CAAA;IACrB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;CACjB,GAAG,MAAM,CAcT;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,+EAA+E;AAC/E,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,iBAAiB,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,EACtF,IAAI,GAAE,aAAkB,GACvB,OAAO,CAAC,cAAc,CAAC,CAWzB;AAED,sDAAsD;AACtD,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,iBAAiB,GAAG;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,EACtE,IAAI,GAAE,aAAkB,GACvB,OAAO,CAAC,cAAc,CAAC,CAUzB;AAED,sFAAsF;AACtF,wBAAgB,6BAA6B,CAC3C,KAAK,EAAE,iBAAiB,GAAG;IAAE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,EAChD,IAAI,GAAE,aAAkB,GACvB,OAAO,CAAC,cAAc,CAAC,CASzB"}
|