Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions packages/junior/tests/fixtures/mcp-oauth-callback-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,35 @@ export async function runMcpOauthCallbackRoute(args: {
}
return response;
}

/** Complete the headless authorization transition and run its callback route. */
export async function completeMcpOauthCallbackRoute(args: {
provider: string;
authSessionId: string;
agentRunner?: AgentRunner;
}) {
const { getMcpAuthSession } = await import("@/chat/mcp/auth-store");
const session = await getMcpAuthSession(args.authSessionId);
if (!session?.authorizationUrl) {
throw new Error(`Missing MCP authorization URL: ${args.authSessionId}`);
}
const authorization = await fetch(session.authorizationUrl, {
redirect: "manual",
});
const location = authorization.headers.get("location");
if (authorization.status !== 302 || !location) {
throw new Error(`MCP authorization failed: ${authorization.status}`);
}
const callback = new URL(location);
const state = callback.searchParams.get("state");
const code = callback.searchParams.get("code");
if (!state || !code) {
throw new Error("MCP authorization callback omitted code or state");
}
return await runMcpOauthCallbackRoute({
provider: args.provider,
state,
code,
...(args.agentRunner ? { agentRunner: args.agentRunner } : {}),
});
}
15 changes: 5 additions & 10 deletions packages/junior/tests/integration/mcp-auth-runtime-slack.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { z } from "zod";
import {
EVAL_MCP_AUTH_CODE,
EVAL_MCP_AUTH_PROVIDER,
} from "../msw/handlers/eval-mcp-auth";
import { EVAL_MCP_AUTH_PROVIDER } from "../msw/handlers/eval-mcp-auth";
import {
getCapturedSlackApiCalls,
resetSlackApiMockState,
Expand Down Expand Up @@ -533,10 +530,9 @@ describe("mcp auth runtime slack integration", () => {
});

const response =
await mcpOauthCallbackHarnessModule.runMcpOauthCallbackRoute({
await mcpOauthCallbackHarnessModule.completeMcpOauthCallbackRoute({
provider: EVAL_MCP_AUTH_PROVIDER,
state: pendingAuthSession!.authSessionId,
code: EVAL_MCP_AUTH_CODE,
authSessionId: pendingAuthSession!.authSessionId,
});

expect(response.status).toBe(200);
Expand Down Expand Up @@ -952,10 +948,9 @@ describe("mcp auth runtime slack integration", () => {
});

const response =
await mcpOauthCallbackHarnessModule.runMcpOauthCallbackRoute({
await mcpOauthCallbackHarnessModule.completeMcpOauthCallbackRoute({
provider: EVAL_MCP_AUTH_PROVIDER,
state: pendingAuthSession!.authSessionId,
code: EVAL_MCP_AUTH_CODE,
authSessionId: pendingAuthSession!.authSessionId,
});

expect(response.status).toBe(200);
Expand Down
35 changes: 13 additions & 22 deletions packages/junior/tests/integration/mcp-oauth-callback-slack.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@ import {
type Actor,
type Source,
} from "@sentry/junior-plugin-api";
import {
EVAL_MCP_AUTH_CODE,
EVAL_MCP_AUTH_PROVIDER,
} from "../msw/handlers/eval-mcp-auth";
import { EVAL_MCP_AUTH_PROVIDER } from "../msw/handlers/eval-mcp-auth";
import {
getCapturedSlackApiCalls,
resetSlackApiMockState,
Expand Down Expand Up @@ -372,10 +369,9 @@ describe("mcp oauth callback slack integration", () => {
});

const response =
await mcpOauthCallbackHarnessModule.runMcpOauthCallbackRoute({
await mcpOauthCallbackHarnessModule.completeMcpOauthCallbackRoute({
provider: EVAL_MCP_AUTH_PROVIDER,
state: authProvider.authSessionId,
code: EVAL_MCP_AUTH_CODE,
authSessionId: authProvider.authSessionId,
agentRunner: testAgentRunner,
});

Expand Down Expand Up @@ -551,10 +547,9 @@ describe("mcp oauth callback slack integration", () => {
});

const response =
await mcpOauthCallbackHarnessModule.runMcpOauthCallbackRoute({
await mcpOauthCallbackHarnessModule.completeMcpOauthCallbackRoute({
provider: EVAL_MCP_AUTH_PROVIDER,
state: authProvider.authSessionId,
code: EVAL_MCP_AUTH_CODE,
authSessionId: authProvider.authSessionId,
agentRunner: testAgentRunner,
});

Expand Down Expand Up @@ -691,10 +686,9 @@ describe("mcp oauth callback slack integration", () => {

try {
const response =
await mcpOauthCallbackHarnessModule.runMcpOauthCallbackRoute({
await mcpOauthCallbackHarnessModule.completeMcpOauthCallbackRoute({
provider: EVAL_MCP_AUTH_PROVIDER,
state: authProvider.authSessionId,
code: EVAL_MCP_AUTH_CODE,
authSessionId: authProvider.authSessionId,
agentRunner: testAgentRunner,
});

Expand Down Expand Up @@ -808,10 +802,9 @@ describe("mcp oauth callback slack integration", () => {
});

const response =
await mcpOauthCallbackHarnessModule.runMcpOauthCallbackRoute({
await mcpOauthCallbackHarnessModule.completeMcpOauthCallbackRoute({
provider: EVAL_MCP_AUTH_PROVIDER,
state: authProvider.authSessionId,
code: EVAL_MCP_AUTH_CODE,
authSessionId: authProvider.authSessionId,
agentRunner: testAgentRunner,
});

Expand Down Expand Up @@ -878,10 +871,9 @@ describe("mcp oauth callback slack integration", () => {
});

const response =
await mcpOauthCallbackHarnessModule.runMcpOauthCallbackRoute({
await mcpOauthCallbackHarnessModule.completeMcpOauthCallbackRoute({
provider: EVAL_MCP_AUTH_PROVIDER,
state: authProvider.authSessionId,
code: EVAL_MCP_AUTH_CODE,
authSessionId: authProvider.authSessionId,
agentRunner: testAgentRunner,
});

Expand Down Expand Up @@ -947,10 +939,9 @@ describe("mcp oauth callback slack integration", () => {
});

const response =
await mcpOauthCallbackHarnessModule.runMcpOauthCallbackRoute({
await mcpOauthCallbackHarnessModule.completeMcpOauthCallbackRoute({
provider: EVAL_MCP_AUTH_PROVIDER,
state: authProvider.authSessionId,
code: EVAL_MCP_AUTH_CODE,
authSessionId: authProvider.authSessionId,
agentRunner: testAgentRunner,
});

Expand Down
92 changes: 87 additions & 5 deletions packages/junior/tests/msw/handlers/eval-mcp-auth.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { createHash } from "node:crypto";
import { http, HttpResponse } from "msw";

export const EVAL_MCP_AUTH_PROVIDER = "eval-auth";
Expand All @@ -12,6 +13,24 @@ const EVAL_MCP_TOKEN_ENDPOINT = `${EVAL_MCP_AUTH_ORIGIN}/oauth/token`;
const EVAL_MCP_REGISTRATION_ENDPOINT = `${EVAL_MCP_AUTH_ORIGIN}/oauth/register`;
const EVAL_MCP_ACCESS_TOKEN = "eval-auth-access-token";
const EVAL_MCP_SESSION_ID = "eval-auth-session";
const EVAL_MCP_CLIENT_ID = "eval-auth-client-id";
const EVAL_MCP_CALLBACK_URL =
"https://junior.example.com/api/oauth/callback/mcp/eval-auth";

interface AuthorizationGrant {
clientId: string;
codeChallenge: string;
redirectUri: string;
scope?: string;
}

let authorizationGrant: AuthorizationGrant | undefined;
let clientRegistered = false;
let tokenIssued = false;

function pkceChallenge(verifier: string): string {
return createHash("sha256").update(verifier).digest("base64url");
}

function unauthorizedResponse() {
return new HttpResponse(null, {
Expand All @@ -35,7 +54,12 @@ function jsonRpcResult(id: unknown, result: unknown, headers?: HeadersInit) {
);
}

export function resetEvalMcpAuthMockState(): void {}
/** Reset headless OAuth state between integration tests. */
export function resetEvalMcpAuthMockState(): void {
authorizationGrant = undefined;
clientRegistered = false;
tokenIssued = false;
}

export const evalMcpAuthHandlers = [
http.get(
Expand Down Expand Up @@ -205,7 +229,7 @@ export const evalMcpAuthHandlers = [
),
http.post(EVAL_MCP_SERVER_URL, async ({ request }) => {
const authorization = request.headers.get("authorization");
if (authorization !== `Bearer ${EVAL_MCP_ACCESS_TOKEN}`) {
if (!tokenIssued || authorization !== `Bearer ${EVAL_MCP_ACCESS_TOKEN}`) {
return unauthorizedResponse();
}

Expand Down Expand Up @@ -332,10 +356,55 @@ export const evalMcpAuthHandlers = [
code_challenge_methods_supported: ["S256"],
}),
),
http.get(EVAL_MCP_AUTHORIZATION_ENDPOINT, async ({ request }) => {
const url = new URL(request.url);
const clientId = url.searchParams.get("client_id");
const redirectUri = url.searchParams.get("redirect_uri");
const state = url.searchParams.get("state");
const codeChallenge = url.searchParams.get("code_challenge");
if (
!clientRegistered ||
clientId !== EVAL_MCP_CLIENT_ID ||
redirectUri !== EVAL_MCP_CALLBACK_URL ||
!state ||
!codeChallenge ||
url.searchParams.get("code_challenge_method") !== "S256" ||
url.searchParams.get("response_type") !== "code"
) {
return HttpResponse.json({ error: "invalid_request" }, { status: 400 });
}

authorizationGrant = {
clientId,
codeChallenge,
redirectUri,
...(url.searchParams.get("scope")
? { scope: url.searchParams.get("scope")! }
: {}),
};
const callback = new URL(redirectUri);
callback.searchParams.set("code", EVAL_MCP_AUTH_CODE);
callback.searchParams.set("state", state);
return new HttpResponse(null, {
status: 302,
headers: { Location: callback.toString() },
});
}),
http.post(EVAL_MCP_REGISTRATION_ENDPOINT, async ({ request }) => {
const body = (await request.json()) as Record<string, unknown>;
if (
!Array.isArray(body.redirect_uris) ||
body.redirect_uris.length !== 1 ||
body.redirect_uris[0] !== EVAL_MCP_CALLBACK_URL
) {
return HttpResponse.json(
{ error: "invalid_client_metadata" },
{ status: 400 },
);
}
clientRegistered = true;
return HttpResponse.json({
client_id: "eval-auth-client-id",
client_id: EVAL_MCP_CLIENT_ID,
client_id_issued_at: Math.floor(Date.now() / 1000),
...(Array.isArray(body.redirect_uris)
? { redirect_uris: body.redirect_uris }
Expand All @@ -360,7 +429,16 @@ export const evalMcpAuthHandlers = [
const bodyText = await request.text();
const params = new URLSearchParams(bodyText);
const code = params.get("code");
if (code !== EVAL_MCP_AUTH_CODE) {
const verifier = params.get("code_verifier");
if (
params.get("grant_type") !== "authorization_code" ||
code !== EVAL_MCP_AUTH_CODE ||
!authorizationGrant ||
params.get("client_id") !== authorizationGrant.clientId ||
params.get("redirect_uri") !== authorizationGrant.redirectUri ||
!verifier ||
pkceChallenge(verifier) !== authorizationGrant.codeChallenge
) {
return HttpResponse.json(
{
error: "invalid_grant",
Expand All @@ -370,12 +448,16 @@ export const evalMcpAuthHandlers = [
);
}

const scope = authorizationGrant.scope ?? "mcp:read";
authorizationGrant = undefined;
tokenIssued = true;

return HttpResponse.json({
access_token: EVAL_MCP_ACCESS_TOKEN,
token_type: "Bearer",
expires_in: 3600,
refresh_token: "eval-auth-refresh-token",
scope: "mcp:read",
scope,
});
}),
];
Loading