diff --git a/admin-dashboard/auth.ts b/admin-dashboard/auth.ts index 52a865c..0d0d2d3 100644 --- a/admin-dashboard/auth.ts +++ b/admin-dashboard/auth.ts @@ -158,4 +158,6 @@ export const { handlers, auth, signIn, signOut } = NextAuth({ }, }, pages: { signIn: "/login" }, -}); +}; + +export const { handlers, auth, signIn, signOut } = NextAuth(authConfig); diff --git a/admin-dashboard/lib/security.test.ts b/admin-dashboard/lib/security.test.ts new file mode 100644 index 0000000..b8daa9a --- /dev/null +++ b/admin-dashboard/lib/security.test.ts @@ -0,0 +1,92 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { NextResponse } from "next/server"; +import { authConfig } from "../auth.ts"; +import { middlewareCallback } from "../middleware.ts"; + +test("NextAuth configuration session cookie matches env", () => { + const isProd = process.env.NODE_ENV === "production"; + assert.equal(authConfig.useSecureCookies, isProd); + + const sessionToken = authConfig.cookies?.sessionToken; + assert.ok(sessionToken); + assert.equal(sessionToken.options.secure, isProd); + if (isProd) { + assert.ok(sessionToken.name.startsWith("__Secure-")); + } else { + assert.ok(!sessionToken.name.startsWith("__Secure-")); + } +}); + +test("middlewareCallback redirects according to environment", () => { + const origNodeEnv = process.env.NODE_ENV; + + try { + // 1. Production HTTPS redirect check + process.env.NODE_ENV = "production"; + const mockReqProd = { + nextUrl: { + pathname: "/admin/dashboard", + protocol: "http:", + }, + url: "http://example.com/admin/dashboard", + headers: { + get: (name: string) => { + if (name === "x-forwarded-proto") return "http"; + return null; + } + }, + auth: null, + } as any; + + const originalRedirect = NextResponse.redirect; + let redirectedTo: string | null = null; + let redirectStatus: number | null = null; + + NextResponse.redirect = (url: string | URL, status?: number) => { + redirectedTo = url.toString(); + redirectStatus = status || null; + return { type: "redirect" } as any; + }; + + try { + const res = middlewareCallback(mockReqProd); + assert.ok(res); + assert.equal(redirectedTo, "https://example.com/admin/dashboard"); + assert.equal(redirectStatus, 301); + } finally { + NextResponse.redirect = originalRedirect; + } + + // 2. Development HTTP validation check (should not redirect) + process.env.NODE_ENV = "development"; + const mockReqDev = { + nextUrl: { + pathname: "/admin/dashboard", + protocol: "http:", + }, + url: "http://example.com/admin/dashboard", + headers: { + get: (name: string) => null, + }, + auth: null, + } as any; + + let redirected = false; + NextResponse.redirect = (url: string | URL, status?: number) => { + redirected = true; + return { type: "redirect" } as any; + }; + + try { + const res = middlewareCallback(mockReqDev); + assert.ok(!redirected); + } finally { + NextResponse.redirect = originalRedirect; + } + + } finally { + process.env.NODE_ENV = origNodeEnv; + } +}); +export {}; diff --git a/admin-dashboard/middleware.ts b/admin-dashboard/middleware.ts index 97d6cf1..79b5fbb 100644 --- a/admin-dashboard/middleware.ts +++ b/admin-dashboard/middleware.ts @@ -1,10 +1,22 @@ import { auth } from "@/auth"; import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; -export default auth((req) => { +export const middlewareCallback = (req: NextRequest & { auth: any }) => { const { pathname } = req.nextUrl; const session = req.auth; + // Force redirect to HTTPS in production + if (process.env.NODE_ENV === "production") { + const xForwardedProto = req.headers.get("x-forwarded-proto"); + const isHttp = req.nextUrl.protocol === "http:" || xForwardedProto === "http"; + if (isHttp) { + const secureUrl = new URL(req.url); + secureUrl.protocol = "https:"; + return NextResponse.redirect(secureUrl.toString(), 301); + } + } + // Protect all /admin/* routes if (pathname.startsWith("/admin")) { if (!session) { @@ -20,7 +32,9 @@ export default auth((req) => { } return NextResponse.next(); -}); +}; + +export default auth(middlewareCallback); // Matcher excludes static assets and Next.js internals automatically export const config = { diff --git a/admin-dashboard/package.json b/admin-dashboard/package.json index 65fd07e..bf013cf 100644 --- a/admin-dashboard/package.json +++ b/admin-dashboard/package.json @@ -1,6 +1,7 @@ { "name": "admin-dashboard", "version": "0.1.1", + "type": "module", "private": true, "scripts": { "dev": "next dev -p 3001",