@softeria/ms-365-mcp-server 0.150.0 → 0.150.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/lib/microsoft-auth.js +67 -54
- package/dist/server.js +16 -11
- package/package.json +1 -1
|
@@ -114,6 +114,61 @@ function toOAuthErrorResponse(error) {
|
|
|
114
114
|
}
|
|
115
115
|
};
|
|
116
116
|
}
|
|
117
|
+
const PUBLIC_CLIENT_SECRET_REJECTED = 700025;
|
|
118
|
+
function isPublicClientRejection(body) {
|
|
119
|
+
return Array.isArray(body?.error_codes) && body.error_codes.includes(PUBLIC_CLIENT_SECRET_REJECTED);
|
|
120
|
+
}
|
|
121
|
+
async function postTokenRequest(tokenUrl, params) {
|
|
122
|
+
const response = await fetch(tokenUrl, {
|
|
123
|
+
method: "POST",
|
|
124
|
+
headers: {
|
|
125
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
126
|
+
},
|
|
127
|
+
body: params
|
|
128
|
+
});
|
|
129
|
+
if (response.ok) {
|
|
130
|
+
return { ok: true, json: await response.json() };
|
|
131
|
+
}
|
|
132
|
+
const raw = await response.text();
|
|
133
|
+
return { ok: false, status: response.status, raw, parsed: parseUpstreamOAuthError(raw) };
|
|
134
|
+
}
|
|
135
|
+
async function requestToken(tokenUrl, params, clientSecret, failureMessage) {
|
|
136
|
+
if (!clientSecret) {
|
|
137
|
+
return unwrapToken(await postTokenRequest(tokenUrl, params), failureMessage);
|
|
138
|
+
}
|
|
139
|
+
const withSecret = new URLSearchParams(params);
|
|
140
|
+
withSecret.append("client_secret", clientSecret);
|
|
141
|
+
let result = await postTokenRequest(tokenUrl, withSecret);
|
|
142
|
+
if (!result.ok && isPublicClientRejection(result.parsed)) {
|
|
143
|
+
logger.info(
|
|
144
|
+
"Upstream rejected client_secret as a public client (AADSTS700025) \u2014 retrying without it",
|
|
145
|
+
{
|
|
146
|
+
status: result.status,
|
|
147
|
+
error_codes: result.parsed?.error_codes,
|
|
148
|
+
correlation_id: result.parsed?.correlation_id
|
|
149
|
+
}
|
|
150
|
+
);
|
|
151
|
+
result = await postTokenRequest(tokenUrl, params);
|
|
152
|
+
}
|
|
153
|
+
return unwrapToken(result, failureMessage);
|
|
154
|
+
}
|
|
155
|
+
function unwrapToken(result, failureMessage) {
|
|
156
|
+
if (result.ok) {
|
|
157
|
+
return result.json;
|
|
158
|
+
}
|
|
159
|
+
if (result.parsed) {
|
|
160
|
+
logger.warn(`Token endpoint upstream OAuth error: ${result.parsed.error}`, {
|
|
161
|
+
status: result.status,
|
|
162
|
+
error: result.parsed.error,
|
|
163
|
+
suberror: result.parsed.suberror,
|
|
164
|
+
error_codes: result.parsed.error_codes,
|
|
165
|
+
correlation_id: result.parsed.correlation_id
|
|
166
|
+
});
|
|
167
|
+
throw new OAuthUpstreamError(result.status, result.raw, result.parsed);
|
|
168
|
+
}
|
|
169
|
+
logger.error(`${failureMessage}: ${result.raw}`);
|
|
170
|
+
throw new Error(`${failureMessage}: ${result.raw}`);
|
|
171
|
+
}
|
|
117
172
|
async function exchangeCodeForToken(code, redirectUri, clientId, clientSecret, tenantId = "common", codeVerifier, cloudType = "global") {
|
|
118
173
|
const cloudEndpoints = getCloudEndpoints(cloudType);
|
|
119
174
|
const params = new URLSearchParams({
|
|
@@ -122,36 +177,15 @@ async function exchangeCodeForToken(code, redirectUri, clientId, clientSecret, t
|
|
|
122
177
|
redirect_uri: redirectUri,
|
|
123
178
|
client_id: clientId
|
|
124
179
|
});
|
|
125
|
-
if (clientSecret) {
|
|
126
|
-
params.append("client_secret", clientSecret);
|
|
127
|
-
}
|
|
128
180
|
if (codeVerifier) {
|
|
129
181
|
params.append("code_verifier", codeVerifier);
|
|
130
182
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
});
|
|
138
|
-
if (!response.ok) {
|
|
139
|
-
const raw = await response.text();
|
|
140
|
-
const parsed = parseUpstreamOAuthError(raw);
|
|
141
|
-
if (parsed) {
|
|
142
|
-
logger.warn(`Token endpoint upstream OAuth error: ${parsed.error}`, {
|
|
143
|
-
status: response.status,
|
|
144
|
-
error: parsed.error,
|
|
145
|
-
suberror: parsed.suberror,
|
|
146
|
-
error_codes: parsed.error_codes,
|
|
147
|
-
correlation_id: parsed.correlation_id
|
|
148
|
-
});
|
|
149
|
-
throw new OAuthUpstreamError(response.status, raw, parsed);
|
|
150
|
-
}
|
|
151
|
-
logger.error(`Failed to exchange code for token: ${raw}`);
|
|
152
|
-
throw new Error(`Failed to exchange code for token: ${raw}`);
|
|
153
|
-
}
|
|
154
|
-
return response.json();
|
|
183
|
+
return requestToken(
|
|
184
|
+
`${cloudEndpoints.authority}/${tenantId}/oauth2/v2.0/token`,
|
|
185
|
+
params,
|
|
186
|
+
clientSecret,
|
|
187
|
+
"Failed to exchange code for token"
|
|
188
|
+
);
|
|
155
189
|
}
|
|
156
190
|
async function refreshAccessToken(refreshToken, clientId, clientSecret, tenantId = "common", cloudType = "global") {
|
|
157
191
|
const cloudEndpoints = getCloudEndpoints(cloudType);
|
|
@@ -160,33 +194,12 @@ async function refreshAccessToken(refreshToken, clientId, clientSecret, tenantId
|
|
|
160
194
|
refresh_token: refreshToken,
|
|
161
195
|
client_id: clientId
|
|
162
196
|
});
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
"Content-Type": "application/x-www-form-urlencoded"
|
|
170
|
-
},
|
|
171
|
-
body: params
|
|
172
|
-
});
|
|
173
|
-
if (!response.ok) {
|
|
174
|
-
const raw = await response.text();
|
|
175
|
-
const parsed = parseUpstreamOAuthError(raw);
|
|
176
|
-
if (parsed) {
|
|
177
|
-
logger.warn(`Token endpoint upstream OAuth error: ${parsed.error}`, {
|
|
178
|
-
status: response.status,
|
|
179
|
-
error: parsed.error,
|
|
180
|
-
suberror: parsed.suberror,
|
|
181
|
-
error_codes: parsed.error_codes,
|
|
182
|
-
correlation_id: parsed.correlation_id
|
|
183
|
-
});
|
|
184
|
-
throw new OAuthUpstreamError(response.status, raw, parsed);
|
|
185
|
-
}
|
|
186
|
-
logger.error(`Failed to refresh token: ${raw}`);
|
|
187
|
-
throw new Error(`Failed to refresh token: ${raw}`);
|
|
188
|
-
}
|
|
189
|
-
return response.json();
|
|
197
|
+
return requestToken(
|
|
198
|
+
`${cloudEndpoints.authority}/${tenantId}/oauth2/v2.0/token`,
|
|
199
|
+
params,
|
|
200
|
+
clientSecret,
|
|
201
|
+
"Failed to refresh token"
|
|
202
|
+
);
|
|
190
203
|
}
|
|
191
204
|
export {
|
|
192
205
|
OAuthUpstreamError,
|
package/dist/server.js
CHANGED
|
@@ -45,6 +45,7 @@ function parseHttpOption(httpOption) {
|
|
|
45
45
|
const port = parseInt(httpString) || 3e3;
|
|
46
46
|
return { host: void 0, port };
|
|
47
47
|
}
|
|
48
|
+
const PKCE_MAX_AGE_MS = 60 * 60 * 1e3;
|
|
48
49
|
class MicrosoftGraphServer {
|
|
49
50
|
constructor(authManager, options = {}) {
|
|
50
51
|
this.version = "0.0.0";
|
|
@@ -318,17 +319,18 @@ class MicrosoftGraphServer {
|
|
|
318
319
|
microsoftAuthUrl.searchParams.set(param, value);
|
|
319
320
|
}
|
|
320
321
|
});
|
|
321
|
-
if (clientCodeChallenge
|
|
322
|
-
const serverCodeVerifier = crypto.randomBytes(32).toString("base64url");
|
|
323
|
-
const serverCodeChallenge = crypto.createHash("sha256").update(serverCodeVerifier).digest("base64url");
|
|
322
|
+
if (clientCodeChallenge) {
|
|
324
323
|
const now = Date.now();
|
|
325
|
-
const maxAge = 10 * 60 * 1e3;
|
|
326
|
-
const maxEntries = 1e3;
|
|
327
324
|
for (const [key, value] of this.pkceStore) {
|
|
328
|
-
if (now - value.createdAt >
|
|
325
|
+
if (now - value.createdAt > PKCE_MAX_AGE_MS || value.clientCodeChallenge === clientCodeChallenge) {
|
|
329
326
|
this.pkceStore.delete(key);
|
|
330
327
|
}
|
|
331
328
|
}
|
|
329
|
+
}
|
|
330
|
+
if (clientCodeChallenge && state) {
|
|
331
|
+
const serverCodeVerifier = crypto.randomBytes(32).toString("base64url");
|
|
332
|
+
const serverCodeChallenge = crypto.createHash("sha256").update(serverCodeVerifier).digest("base64url");
|
|
333
|
+
const maxEntries = 1e3;
|
|
332
334
|
if (this.pkceStore.size >= maxEntries) {
|
|
333
335
|
logger.warn(
|
|
334
336
|
`PKCE store at capacity (${maxEntries} entries) \u2014 rejecting new authorization request`
|
|
@@ -341,7 +343,6 @@ class MicrosoftGraphServer {
|
|
|
341
343
|
}
|
|
342
344
|
this.pkceStore.set(state, {
|
|
343
345
|
clientCodeChallenge,
|
|
344
|
-
clientCodeChallengeMethod: clientCodeChallengeMethod || "S256",
|
|
345
346
|
serverCodeVerifier,
|
|
346
347
|
createdAt: Date.now()
|
|
347
348
|
});
|
|
@@ -405,14 +406,15 @@ class MicrosoftGraphServer {
|
|
|
405
406
|
tenantId,
|
|
406
407
|
hasClientSecret: !!clientSecret
|
|
407
408
|
});
|
|
408
|
-
let
|
|
409
|
+
let matchedPkceState;
|
|
410
|
+
let matchedPkceEntry;
|
|
409
411
|
if (body.code_verifier) {
|
|
410
412
|
const clientVerifier = body.code_verifier;
|
|
411
413
|
const clientChallengeComputed = crypto.createHash("sha256").update(clientVerifier).digest("base64url");
|
|
412
414
|
for (const [state, pkceData] of this.pkceStore) {
|
|
413
415
|
if (pkceData.clientCodeChallenge === clientChallengeComputed) {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
+
matchedPkceState = state;
|
|
417
|
+
matchedPkceEntry = pkceData;
|
|
416
418
|
logger.info("Two-leg PKCE: matched client verifier, using server verifier", {
|
|
417
419
|
state: state.substring(0, 8) + "..."
|
|
418
420
|
});
|
|
@@ -426,9 +428,12 @@ class MicrosoftGraphServer {
|
|
|
426
428
|
clientId,
|
|
427
429
|
clientSecret,
|
|
428
430
|
tenantId,
|
|
429
|
-
serverCodeVerifier || body.code_verifier,
|
|
431
|
+
matchedPkceEntry?.serverCodeVerifier || body.code_verifier,
|
|
430
432
|
this.secrets.cloudType
|
|
431
433
|
);
|
|
434
|
+
if (matchedPkceState && this.pkceStore.get(matchedPkceState) === matchedPkceEntry) {
|
|
435
|
+
this.pkceStore.delete(matchedPkceState);
|
|
436
|
+
}
|
|
432
437
|
res.json(result);
|
|
433
438
|
} else if (body.grant_type === "refresh_token") {
|
|
434
439
|
const tenantId = this.secrets?.tenantId || "common";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softeria/ms-365-mcp-server",
|
|
3
3
|
"mcpName": "io.github.Softeria/ms-365-mcp-server",
|
|
4
|
-
"version": "0.150.
|
|
4
|
+
"version": "0.150.1",
|
|
5
5
|
"description": " A Model Context Protocol (MCP) server for interacting with Microsoft 365 and Office services through the Graph API",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|