Sessions from before CSRF was added lack the bh-csrf-token cookie, causing 403 on first save. Middleware now auto-generates the cookie if the user is authenticated but missing it. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
66 lines
2.1 KiB
TypeScript
66 lines
2.1 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { verifyToken, COOKIE_NAME } from "@/lib/auth-edge";
|
|
|
|
const CSRF_COOKIE_NAME = "bh-csrf-token";
|
|
const CSRF_HEADER_NAME = "x-csrf-token";
|
|
const STATE_CHANGING_METHODS = new Set(["POST", "PUT", "DELETE", "PATCH"]);
|
|
|
|
function generateCsrfToken(): string {
|
|
const array = new Uint8Array(32);
|
|
crypto.getRandomValues(array);
|
|
let binary = "";
|
|
for (const b of array) binary += String.fromCharCode(b);
|
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
}
|
|
|
|
export async function proxy(request: NextRequest) {
|
|
const { pathname } = request.nextUrl;
|
|
|
|
// Allow login page and login API
|
|
if (pathname === "/admin/login" || pathname === "/api/auth/login") {
|
|
return NextResponse.next();
|
|
}
|
|
|
|
// Protect /admin/* and /api/admin/*
|
|
const token = request.cookies.get(COOKIE_NAME)?.value;
|
|
const valid = token ? await verifyToken(token) : false;
|
|
|
|
if (!valid) {
|
|
if (pathname.startsWith("/api/")) {
|
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
}
|
|
return NextResponse.redirect(new URL("/admin/login", request.url));
|
|
}
|
|
|
|
// Auto-issue CSRF cookie if missing (e.g. session from before CSRF was added)
|
|
const hasCsrf = request.cookies.has(CSRF_COOKIE_NAME);
|
|
if (!hasCsrf) {
|
|
const csrfToken = generateCsrfToken();
|
|
const response = NextResponse.next();
|
|
response.cookies.set(CSRF_COOKIE_NAME, csrfToken, {
|
|
httpOnly: false,
|
|
secure: process.env.NODE_ENV === "production",
|
|
sameSite: "strict",
|
|
path: "/",
|
|
maxAge: 60 * 60 * 24,
|
|
});
|
|
return response;
|
|
}
|
|
|
|
// CSRF check on state-changing API requests
|
|
if (pathname.startsWith("/api/admin/") && STATE_CHANGING_METHODS.has(request.method)) {
|
|
const csrfCookie = request.cookies.get(CSRF_COOKIE_NAME)?.value;
|
|
const csrfHeader = request.headers.get(CSRF_HEADER_NAME);
|
|
|
|
if (!csrfCookie || !csrfHeader || csrfCookie !== csrfHeader) {
|
|
return NextResponse.json({ error: "CSRF token mismatch" }, { status: 403 });
|
|
}
|
|
}
|
|
|
|
return NextResponse.next();
|
|
}
|
|
|
|
export const config = {
|
|
matcher: ["/admin/:path*", "/api/admin/:path*"],
|
|
};
|