@softeria/ms-365-mcp-server 0.107.0 → 0.107.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.
@@ -0,0 +1,33 @@
1
+ const LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
2
+ function isLoopbackHost(host) {
3
+ return LOOPBACK_HOSTS.has(host.toLowerCase());
4
+ }
5
+ function parseAllowlist(raw) {
6
+ if (!raw) return null;
7
+ const list = raw.split(",").map((s) => s.trim()).filter(Boolean);
8
+ return list.length > 0 ? list : null;
9
+ }
10
+ function isAllowedRedirectUri(value, allowlist) {
11
+ if (typeof value !== "string" || value.length === 0) return false;
12
+ let url;
13
+ try {
14
+ url = new URL(value);
15
+ } catch {
16
+ return false;
17
+ }
18
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
19
+ return false;
20
+ }
21
+ if (allowlist && allowlist.length > 0) {
22
+ return allowlist.includes(value);
23
+ }
24
+ if (url.protocol === "http:") {
25
+ return isLoopbackHost(url.hostname);
26
+ }
27
+ return true;
28
+ }
29
+ export {
30
+ isAllowedRedirectUri,
31
+ isLoopbackHost,
32
+ parseAllowlist
33
+ };
package/dist/server.js CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  microsoftBearerTokenAuthMiddleware,
16
16
  refreshAccessToken
17
17
  } from "./lib/microsoft-auth.js";
18
+ import { isAllowedRedirectUri, parseAllowlist } from "./lib/redirect-uri-validation.js";
18
19
  import { getSecrets } from "./secrets.js";
19
20
  import { getCloudEndpoints } from "./cloud-config.js";
20
21
  import { requestContext } from "./request-context.js";
@@ -169,7 +170,11 @@ class MicrosoftGraphServer {
169
170
  const protocol = req.secure ? "https" : "http";
170
171
  const requestOrigin = `${protocol}://${req.get("host")}`;
171
172
  const browserBase = publicBase ?? requestOrigin;
172
- const scopes = buildScopesFromEndpoints(this.options.orgMode, this.options.enabledTools);
173
+ const scopes = buildScopesFromEndpoints(
174
+ this.options.orgMode,
175
+ this.options.enabledTools,
176
+ this.options.readOnly
177
+ );
173
178
  const metadata = {
174
179
  issuer: browserBase,
175
180
  authorization_endpoint: `${browserBase}/authorize`,
@@ -190,7 +195,11 @@ class MicrosoftGraphServer {
190
195
  const protocol = req.secure ? "https" : "http";
191
196
  const requestOrigin = `${protocol}://${req.get("host")}`;
192
197
  const browserBase = publicBase ?? requestOrigin;
193
- const scopes = this.options.obo ? [`api://${this.secrets.clientId}/access_as_user`] : buildScopesFromEndpoints(this.options.orgMode, this.options.enabledTools);
198
+ const scopes = this.options.obo ? [`api://${this.secrets.clientId}/access_as_user`] : buildScopesFromEndpoints(
199
+ this.options.orgMode,
200
+ this.options.enabledTools,
201
+ this.options.readOnly
202
+ );
194
203
  res.json({
195
204
  resource: `${requestOrigin}/mcp`,
196
205
  authorization_servers: [browserBase],
@@ -226,6 +235,20 @@ class MicrosoftGraphServer {
226
235
  const clientCodeChallenge = url.searchParams.get("code_challenge");
227
236
  const clientCodeChallengeMethod = url.searchParams.get("code_challenge_method");
228
237
  const state = url.searchParams.get("state");
238
+ const redirectUriParam = url.searchParams.get("redirect_uri");
239
+ if (redirectUriParam) {
240
+ const allowlist = parseAllowlist(process.env.MS365_MCP_ALLOWED_REDIRECT_URIS);
241
+ if (!isAllowedRedirectUri(redirectUriParam, allowlist)) {
242
+ logger.warn("Rejected /authorize request with disallowed redirect_uri", {
243
+ redirect_uri: redirectUriParam
244
+ });
245
+ res.status(400).json({
246
+ error: "invalid_request",
247
+ error_description: "redirect_uri is not allowed"
248
+ });
249
+ return;
250
+ }
251
+ }
229
252
  const allowedParams = [
230
253
  "response_type",
231
254
  "redirect_uri",
@@ -281,19 +304,14 @@ class MicrosoftGraphServer {
281
304
  }
282
305
  }
283
306
  microsoftAuthUrl.searchParams.set("client_id", clientId);
284
- if (!microsoftAuthUrl.searchParams.get("scope")) {
285
- microsoftAuthUrl.searchParams.set(
286
- "scope",
287
- "User.Read Files.Read Mail.Read offline_access"
288
- );
289
- } else {
290
- const scopeValue = microsoftAuthUrl.searchParams.get("scope");
291
- const scopeList = scopeValue.split(/\s+/).filter(Boolean);
292
- if (!scopeList.includes("offline_access")) {
293
- scopeList.push("offline_access");
294
- microsoftAuthUrl.searchParams.set("scope", scopeList.join(" "));
295
- }
296
- }
307
+ const clientScope = microsoftAuthUrl.searchParams.get("scope");
308
+ const baseScopes = clientScope ? clientScope.split(/\s+/).filter(Boolean) : buildScopesFromEndpoints(
309
+ this.options.orgMode,
310
+ this.options.enabledTools,
311
+ this.options.readOnly
312
+ );
313
+ const scopeSet = /* @__PURE__ */ new Set([...baseScopes, "User.Read", "offline_access"]);
314
+ microsoftAuthUrl.searchParams.set("scope", Array.from(scopeSet).join(" "));
297
315
  res.redirect(microsoftAuthUrl.toString());
298
316
  });
299
317
  app.post("/token", async (req, res) => {
@@ -130,6 +130,28 @@ When deploying for an organization, create a dedicated app registration instead
130
130
 
131
131
  5. **Store credentials** in Key Vault (see [Azure Key Vault Integration](../README.md#azure-key-vault-integration))
132
132
 
133
+ ## Redirect URI Validation
134
+
135
+ The /authorize endpoint defensively validates client-supplied `redirect_uri` values before forwarding them to Microsoft Entra (CWE-601, Open Redirect). Microsoft Entra also validates the URI against your app registration, but this server-side check rejects obviously dangerous schemes (`javascript:`, `data:`, `file:`, …) and arbitrary remote `http://` origins before the request leaves the server.
136
+
137
+ Default behaviour (no explicit allowlist):
138
+
139
+ - Only `http:` and `https:` schemes are accepted.
140
+ - `http:` is only allowed for loopback hosts (`localhost`, `127.0.0.1`, `::1`).
141
+ - All other `https://` origins are accepted (Entra still has the final say).
142
+
143
+ For production deployments, configure an explicit allowlist via the `MS365_MCP_ALLOWED_REDIRECT_URIS` environment variable. It takes a comma-separated list of exact URIs; only exact string matches pass validation:
144
+
145
+ ```bash
146
+ # Single redirect URI
147
+ MS365_MCP_ALLOWED_REDIRECT_URIS=https://mcp.example.com/auth/callback
148
+
149
+ # Multiple URIs (comma-separated, no spaces required)
150
+ MS365_MCP_ALLOWED_REDIRECT_URIS=https://mcp.example.com/auth/callback,https://staging.example.com/auth/callback
151
+ ```
152
+
153
+ The list should mirror the redirect URIs registered on your Azure AD app registration. Leaving the variable unset falls back to the default behaviour above, which is appropriate for local development but not recommended for shared/production deployments.
154
+
133
155
  ## Reverse Proxy / Custom Domain
134
156
 
135
157
  When running behind a reverse proxy, set `MS365_MCP_PUBLIC_URL` so that the OAuth authorize URL handed back to the user's browser is resolvable from outside the server's network:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softeria/ms-365-mcp-server",
3
- "version": "0.107.0",
3
+ "version": "0.107.2",
4
4
  "description": " A Model Context Protocol (MCP) server for interacting with Microsoft 365 and Office services through the Graph API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",