diff --git a/Dockerfile b/Dockerfile index 897c9eb..009bc3d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,6 +4,7 @@ WORKDIR /src COPY annonet-arrowstack-ready.zip /tmp/annonet.zip RUN unzip -q /tmp/annonet.zip -d /src COPY mcpAuthMiddleware.js /src/src/auth/mcpAuthMiddleware.js +COPY oauthProbe.js /src/oauthProbe.js # Arrowstack's database-per-tenant runtime role intentionally cannot install # extensions. PostgreSQL 13+ provides gen_random_uuid() in core, and annoNet # does not use any other pgcrypto functions. @@ -32,6 +33,7 @@ COPY --from=source /src/src ./src COPY --from=source /src/sql ./sql COPY --from=source /src/scripts ./scripts COPY --from=source /src/frontend ./frontend +COPY --from=source /src/oauthProbe.js ./oauthProbe.js COPY --from=frontend-build /build/frontend-astro/dist ./public RUN npm run check \ diff --git a/oauthProbe.js b/oauthProbe.js new file mode 100644 index 0000000..7c67929 --- /dev/null +++ b/oauthProbe.js @@ -0,0 +1,68 @@ +const decodePayload = (token) => { + try { + const part = String(token || "").split(".")[1] || ""; + return JSON.parse(Buffer.from(part, "base64url").toString("utf8")); + } catch { + return {}; + } +}; + +const call = async (label, token) => { + const headers = token === undefined ? {} : { Authorization: `Bearer ${token}` }; + const response = await fetch("http://127.0.0.1:8787/api/mcp/annotations?limit=1", { headers }); + const body = await response.text(); + console.log("[oauth-probe]", JSON.stringify({ + test: label, + status: response.status, + body: body.slice(0, 240) + })); + return response.status; +}; + +(async () => { + const issuer = String(process.env.MCP_TEST_ISSUER || "").replace(/\/$/, ""); + const clientId = String(process.env.MCP_TEST_CLIENT_ID || ""); + const clientSecret = String(process.env.MCP_TEST_CLIENT_SECRET || ""); + if (!issuer || !clientId || !clientSecret) { + throw new Error("temporary MCP test credentials are not configured"); + } + + const tokenResponse = await fetch(`${issuer}/protocol/openid-connect/token`, { + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ + grant_type: "client_credentials", + client_id: clientId, + client_secret: clientSecret + }) + }); + const tokenBody = await tokenResponse.json().catch(() => ({})); + console.log("[oauth-probe]", JSON.stringify({ + test: "client_credentials", + status: tokenResponse.status, + tokenType: tokenBody.token_type || null, + expiresIn: tokenBody.expires_in || null + })); + if (!tokenResponse.ok || !tokenBody.access_token) { + process.exitCode = 2; + return; + } + + const claims = decodePayload(tokenBody.access_token); + console.log("[oauth-probe]", JSON.stringify({ + test: "safe_claims", + iss: claims.iss || null, + azp: claims.azp || claims.client_id || null, + aud: claims.aud || null, + preferredUsername: claims.preferred_username || null, + realmRoles: claims.realm_access && claims.realm_access.roles || [], + resourceAccess: claims.resource_access || {} + })); + + await call("missing_token", undefined); + await call("invalid_token", "not-a-jwt"); + await call("service_account", tokenBody.access_token); +})().catch((error) => { + console.error("[oauth-probe-error]", error && error.message ? error.message : error); + process.exitCode = 3; +});