mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-04 02:04:06 +00:00
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
133 lines
4.2 KiB
TypeScript
133 lines
4.2 KiB
TypeScript
vi.mock("@/lib/registry/access.server", () => ({
|
|
evaluateRegistryAccess: vi.fn(),
|
|
}));
|
|
import type { NextAuthRequest } from "next-auth";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
|
|
vi.mock("next-auth", () => ({
|
|
default: vi.fn(() => ({
|
|
signIn: vi.fn(),
|
|
signOut: vi.fn(),
|
|
auth: vi.fn(),
|
|
handlers: {},
|
|
})),
|
|
}));
|
|
|
|
vi.mock("next-auth/providers/credentials", () => ({
|
|
default: vi.fn((config) => config),
|
|
}));
|
|
|
|
vi.mock("@/auth.config", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("@/auth.config")>();
|
|
|
|
return {
|
|
...actual,
|
|
auth: vi.fn(
|
|
(handler: (request: NextAuthRequest) => Response | Promise<Response>) =>
|
|
async (request: NextAuthRequest) => {
|
|
// Match NextAuth's production order: `authorized` can return a
|
|
// response before the wrapped proxy handler is invoked.
|
|
const authorization = await actual.authConfig.callbacks?.authorized?.(
|
|
{
|
|
auth: request.auth,
|
|
request,
|
|
},
|
|
);
|
|
|
|
if (authorization instanceof Response) return authorization;
|
|
|
|
return handler(request);
|
|
},
|
|
),
|
|
};
|
|
});
|
|
vi.mock("@/lib/csp", () => ({ getCspHeader: () => "default-src 'self'" }));
|
|
vi.mock("@/lib/integrations", () => ({
|
|
GATED_INTEGRATIONS: { posthog: "posthog" },
|
|
isGatedIntegrationEnabled: () => false,
|
|
readGatedEnv: () => undefined,
|
|
}));
|
|
vi.mock("@/lib/runtime-env", () => ({ readEnv: () => undefined }));
|
|
vi.mock("@/lib/shared/env", () => ({ isCloud: () => true }));
|
|
|
|
import proxy from "./proxy";
|
|
|
|
const CALLBACK_URL =
|
|
"https://cloud.prowler.com/integrations/slack/callback" +
|
|
"?code=slack-code-1f4a&state=st-2f1c9d7a";
|
|
|
|
const invokeProxy = async (
|
|
auth: NextAuthRequest["auth"],
|
|
href = CALLBACK_URL,
|
|
): Promise<Response> => {
|
|
const url = new URL(href);
|
|
const request = {
|
|
auth,
|
|
nextUrl: url,
|
|
url: url.toString(),
|
|
} as NextAuthRequest;
|
|
|
|
return (proxy as unknown as (request: NextAuthRequest) => Promise<Response>)(
|
|
request,
|
|
);
|
|
};
|
|
|
|
describe("Slack OAuth callback authentication", () => {
|
|
it.each([
|
|
{
|
|
label: "the session expired",
|
|
auth: { error: "RefreshAccessTokenError" } as NextAuthRequest["auth"],
|
|
},
|
|
{ label: "the session is missing", auth: null },
|
|
])(
|
|
"strips the OAuth credentials before sign-in when $label",
|
|
async ({ auth }) => {
|
|
// Given - Slack returned a single-use code to an unauthenticated callback.
|
|
|
|
// When
|
|
const response = await invokeProxy(auth);
|
|
|
|
// Then - sign-in resumes on a clean integration URL that asks for a new install.
|
|
const location = new URL(response.headers.get("location") as string);
|
|
expect(location.pathname).toBe("/sign-in");
|
|
expect(location.searchParams.get("callbackUrl")).toBe(
|
|
"/integrations/slack?slack=expired",
|
|
);
|
|
expect(location.href).not.toContain("slack-code-1f4a");
|
|
expect(location.href).not.toContain("st-2f1c9d7a");
|
|
},
|
|
);
|
|
|
|
it("keeps any other page's own query, so sign-in still returns where the user was", async () => {
|
|
// Given - an ordinary protected page carrying state worth resuming on.
|
|
const findings = "https://cloud.prowler.com/findings?severity=critical";
|
|
|
|
// When
|
|
const response = await invokeProxy(null, findings);
|
|
|
|
// Then - only the callback's credentials are dropped, nothing else.
|
|
const location = new URL(response.headers.get("location") as string);
|
|
expect(location.searchParams.get("callbackUrl")).toBe(
|
|
"/findings?severity=critical",
|
|
);
|
|
});
|
|
|
|
it("is answered by the authorized callback, never by the proxy behind it", async () => {
|
|
// Given - the proxy is the only layer that attaches the security headers,
|
|
// which makes it observable whether it ran at all.
|
|
|
|
// When
|
|
const turnedAway = await invokeProxy(null);
|
|
const allowed = await invokeProxy({
|
|
user: { permissions: { manage_integrations: true } },
|
|
} as NextAuthRequest["auth"]);
|
|
|
|
// Then - an unauthenticated request is settled before the proxy is reached,
|
|
// so a fix that lands only there would never run in production.
|
|
expect(turnedAway.headers.get("content-security-policy")).toBeNull();
|
|
expect(allowed.headers.get("content-security-policy")).toBe(
|
|
"default-src 'self'",
|
|
);
|
|
});
|
|
});
|