mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-22 05:26:17 +00:00
fix(web): validate proxy sessions
This commit is contained in:
parent
2cebe81512
commit
43a9ebd7d6
2 changed files with 122 additions and 6 deletions
95
apps/web/middleware.test.ts
Normal file
95
apps/web/middleware.test.ts
Normal file
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
@ -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"])
|
||||
|
|
@ -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)
|
||||
|
|
@ -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())
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue