Skip to content
Open
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
95 changes: 95 additions & 0 deletions apps/web/middleware.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { beforeEach, describe, expect, it, mock } from "bun:test"

const getSession = mock()

mock.module("@lib/auth.middleware", () => ({
middlewareAuthClient: { getSession },
}))

const { default: proxy } = await import("./middleware")

beforeEach(() => {
getSession.mockReset()
})

describe("auth proxy", () => {
it("rejects API requests with a forged session cookie", async () => {
getSession.mockResolvedValue(null)

const response = await proxy(
new Request("https://app.supermemory.ai/api/onboarding/research", {
headers: {
cookie: "better-auth.session_token=forged",
},
}),
)

expect(response.status).toBe(401)
expect(await response.json()).toEqual({ error: "Unauthorized" })
expect(getSession).toHaveBeenCalledWith({
fetchOptions: {
headers: {
cookie: "better-auth.session_token=forged",
},
},
})
})

it("does not validate API requests without a session cookie", async () => {
const response = await proxy(
new Request("https://app.supermemory.ai/api/onboarding/research"),
)

expect(response.status).toBe(401)
expect(getSession).not.toHaveBeenCalled()
})

it("redirects page requests with an expired session cookie", async () => {
getSession.mockResolvedValue(null)

const response = await proxy(
new Request("https://app.supermemory.ai/settings", {
headers: {
cookie: "better-auth.session_token=expired",
},
}),
)

expect(response.status).toBe(307)
expect(response.headers.get("location")).toBe(
"https://app.supermemory.ai/login?redirect=https%3A%2F%2Fapp.supermemory.ai%2Fsettings",
)
})

it("allows API requests with a valid session", async () => {
getSession.mockResolvedValue({
session: {},
user: {},
})

const response = await proxy(
new Request("https://app.supermemory.ai/api/onboarding/research", {
headers: {
cookie: "better-auth.session_token=valid",
},
}),
)

expect(response.status).toBe(200)
expect(response.headers.get("x-middleware-next")).toBe("1")
})

it("fails closed when session validation is unavailable", async () => {
getSession.mockRejectedValue(new Error("backend unavailable"))

const response = await proxy(
new Request("https://app.supermemory.ai/api/onboarding/research", {
headers: {
cookie: "better-auth.session_token=unknown",
},
}),
)

expect(response.status).toBe(401)
})
})
33 changes: 27 additions & 6 deletions apps/web/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getSessionCookie } from "better-auth/cookies"
import { NextResponse } from "next/server"
import { middlewareAuthClient } from "@lib/auth.middleware"
import { getPublicRequestUrl } from "@/lib/url-helpers"

const LOCAL_DEV_HOSTS = new Set(["localhost", "127.0.0.1", "::1"])
Expand All @@ -11,6 +12,22 @@ function getAuthSessionCookie(request: Request): string | null {
)
}

async function hasValidSession(request: Request): Promise<boolean> {
try {
const session = await middlewareAuthClient.getSession({
fetchOptions: {
headers: {
cookie: request.headers.get("cookie") ?? "",
},
},
})
return Boolean(session?.session && session.user)
} catch (error) {
console.error("[PROXY] Failed to validate session", error)
return false
}
}

export default async function proxy(request: Request) {
console.debug("[PROXY] === PROXY START ===")
const url = getPublicRequestUrl(request)
Expand Down Expand Up @@ -58,22 +75,26 @@ export default async function proxy(request: Request) {
return NextResponse.next()
}

const validSession = sessionCookie ? await hasValidSession(request) : false

if (url.pathname.startsWith("/api/")) {
if (!sessionCookie) {
console.debug("[MIDDLEWARE] API route without session, returning 401")
if (!validSession) {
console.debug(
"[MIDDLEWARE] API route without valid session, returning 401",
)
return new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 401,
headers: { "Content-Type": "application/json" },
})
}
console.debug("[MIDDLEWARE] API route with session, allowing access")
console.debug("[MIDDLEWARE] API route with valid session, allowing access")
return NextResponse.next()
}

// If no session cookie and not on a public path, redirect to login
if (!sessionCookie) {
// If no valid session and not on a public path, redirect to login
if (!validSession) {
console.debug(
"[PROXY] No session cookie and not on public path, redirecting to /login",
"[PROXY] No valid session and not on public path, redirecting to /login",
)
const loginUrl = new URL("/login", url.origin)
loginUrl.searchParams.set("redirect", url.toString())
Expand Down
Loading