From 8c6e954fdf24c9365e6324b8ca84a50f322506ab Mon Sep 17 00:00:00 2001 From: Darren Clarke Date: Wed, 4 Sep 2024 12:09:28 +0200 Subject: [PATCH] Clean up middleware, add security-headers to non-Zammad pages --- apps/link/app/(main)/_components/Sidebar.tsx | 2 +- .../[id]/@detail/_components/TicketDetail.tsx | 2 +- apps/link/app/_components/MultiProvider.tsx | 6 +- ...RFProvider.tsx => ZammadLoginProvider.tsx} | 10 +-- apps/link/app/_lib/authentication.ts | 15 +--- apps/link/app/_lib/zammad.ts | 3 - apps/link/app/layout.tsx | 2 + apps/link/middleware.ts | 74 ++++++++++++------- apps/link/next.config.js | 29 ++++++-- 9 files changed, 81 insertions(+), 62 deletions(-) rename apps/link/app/_components/{CSRFProvider.tsx => ZammadLoginProvider.tsx} (65%) diff --git a/apps/link/app/(main)/_components/Sidebar.tsx b/apps/link/app/(main)/_components/Sidebar.tsx index c8c546e..6c8af30 100644 --- a/apps/link/app/(main)/_components/Sidebar.tsx +++ b/apps/link/app/(main)/_components/Sidebar.tsx @@ -202,7 +202,7 @@ export const Sidebar: FC = ({ fetchCounts(); - const interval = setInterval(fetchCounts, 10000); + const interval = setInterval(fetchCounts, 30000); return () => clearInterval(interval); }, []); diff --git a/apps/link/app/(main)/tickets/[id]/@detail/_components/TicketDetail.tsx b/apps/link/app/(main)/tickets/[id]/@detail/_components/TicketDetail.tsx index bc6f6b7..d24d34a 100644 --- a/apps/link/app/(main)/tickets/[id]/@detail/_components/TicketDetail.tsx +++ b/apps/link/app/(main)/tickets/[id]/@detail/_components/TicketDetail.tsx @@ -47,7 +47,7 @@ export const TicketDetail: FC = ({ id }) => { fetchTicketArticles(); - const interval = setInterval(fetchTicketArticles, 2000); + const interval = setInterval(fetchTicketArticles, 5000); return () => clearInterval(interval); }, [id]); diff --git a/apps/link/app/_components/MultiProvider.tsx b/apps/link/app/_components/MultiProvider.tsx index b526799..896d3de 100644 --- a/apps/link/app/_components/MultiProvider.tsx +++ b/apps/link/app/_components/MultiProvider.tsx @@ -9,7 +9,7 @@ import { AdapterDateFns } from "@mui/x-date-pickers-pro/AdapterDateFnsV3"; import { LocalizationProvider } from "@mui/x-date-pickers-pro"; import { LicenseInfo } from "@mui/x-license"; import { locales, LeafcutterProvider } from "@link-stack/leafcutter-ui"; -import { CSRFProvider } from "./CSRFProvider"; +import { ZammadLoginProvider } from "./ZammadLoginProvider"; LicenseInfo.setLicenseKey( "c787ac6613c5f2aa0494c4285fe3e9f2Tz04OTY1NyxFPTE3NDYzNDE0ODkwMDAsUz1wcm8sTE09c3Vic2NyaXB0aW9uLEtWPTI=", @@ -22,7 +22,7 @@ export const MultiProvider: FC = ({ children }) => { return ( - + @@ -30,7 +30,7 @@ export const MultiProvider: FC = ({ children }) => { - + ); }; diff --git a/apps/link/app/_components/CSRFProvider.tsx b/apps/link/app/_components/ZammadLoginProvider.tsx similarity index 65% rename from apps/link/app/_components/CSRFProvider.tsx rename to apps/link/app/_components/ZammadLoginProvider.tsx index 1ef6683..b877071 100644 --- a/apps/link/app/_components/CSRFProvider.tsx +++ b/apps/link/app/_components/ZammadLoginProvider.tsx @@ -4,25 +4,19 @@ import { FC, PropsWithChildren, useEffect } from "react"; import { useRouter } from "next/navigation"; import { useSession } from "next-auth/react"; -export const CSRFProvider: FC = ({ children }) => { +export const ZammadLoginProvider: FC = ({ children }) => { const { data: session, status, update } = useSession(); const router = useRouter(); useEffect(() => { const checkSession = async () => { - console.log("Checking session status..."); - console.log(status); if (status === "authenticated") { const response = await fetch("/api/v1/users/me", { method: "GET", }); - if (response.status !== 200 && !!router) { - console.log("redirecting"); + if (response.status !== 200) { window.location.href = "/auth/sso"; - } else { - const token = response.headers.get("CSRF-Token"); - update({ zammadCsrfToken: token }); } } }; diff --git a/apps/link/app/_lib/authentication.ts b/apps/link/app/_lib/authentication.ts index 2707270..8029a06 100644 --- a/apps/link/app/_lib/authentication.ts +++ b/apps/link/app/_lib/authentication.ts @@ -101,30 +101,19 @@ export const authOptions: NextAuthOptions = { callbacks: { signIn: async ({ user }) => { const roles = (await getUserRoles(user.email)) ?? []; - return ( - roles.includes("admin") || - roles.includes("agent") || - process.env.SETUP_MODE === "true" - ); + return roles.includes("admin") || roles.includes("agent"); }, session: async ({ session, token }) => { // @ts-ignore session.user.roles = token.roles ?? []; - // @ts-ignore - session.user.leafcutter = token.leafcutter; // remove - // @ts-ignore - session.user.zammadCsrfToken = token.zammadCsrfToken; return session; }, - jwt: async ({ token, user, trigger, session }) => { + jwt: async ({ token, user }) => { if (user) { token.roles = (await getUserRoles(user.email)) ?? []; } - if (session && trigger === "update") { - token.zammadCsrfToken = session.zammadCsrfToken; - } return token; }, }, diff --git a/apps/link/app/_lib/zammad.ts b/apps/link/app/_lib/zammad.ts index 273ea4d..cd2422d 100644 --- a/apps/link/app/_lib/zammad.ts +++ b/apps/link/app/_lib/zammad.ts @@ -1,5 +1,4 @@ import { getServerSession } from "app/_lib/authentication"; -import { redirect } from "next/navigation"; import { cookies } from "next/headers"; const getHeaders = async () => { @@ -8,8 +7,6 @@ const getHeaders = async () => { const headers = { "Content-Type": "application/json", Accept: "application/json", - // @ts-ignore - "X-CSRF-Token": session.user.zammadCsrfToken, "X-Browser-Fingerprint": `${session.expires}`, Cookie: allCookies .map((cookie: any) => `${cookie.name}=${cookie.value}`) diff --git a/apps/link/app/layout.tsx b/apps/link/app/layout.tsx index b61dab0..a14710d 100644 --- a/apps/link/app/layout.tsx +++ b/apps/link/app/layout.tsx @@ -4,6 +4,8 @@ import { AppRouterCacheProvider } from "@mui/material-nextjs/v14-appRouter"; import { MultiProvider } from "./_components/MultiProvider"; import "./_styles/global.css"; +export const dynamic = "force-dynamic"; + export const metadata: Metadata = { title: "CDR Link", }; diff --git a/apps/link/middleware.ts b/apps/link/middleware.ts index 3eaa8d1..b54b394 100644 --- a/apps/link/middleware.ts +++ b/apps/link/middleware.ts @@ -10,11 +10,12 @@ const rewriteURL = ( const destinationURL = request.url.replace(originBaseURL, destinationBaseURL); console.log(`Rewriting ${request.url} to ${destinationURL}`); const requestHeaders = new Headers(request.headers); + requestHeaders.delete("x-forwarded-user"); + requestHeaders.delete("connection"); for (const [key, value] of Object.entries(headers)) { requestHeaders.set(key, value as string); } - requestHeaders.delete("connection"); return NextResponse.rewrite(new URL(destinationURL), { request: { headers: requestHeaders }, @@ -24,11 +25,9 @@ const rewriteURL = ( const checkRewrites = async (request: NextRequestWithAuth) => { const linkBaseURL = process.env.LINK_URL ?? "http://localhost:3000"; const zammadURL = process.env.ZAMMAD_URL ?? "http://zammad-nginx:8080"; - const opensearchDashboardsURL = - process.env.OPENSEARCH_DASHBOARDS_URL ?? "http://macmini:5601"; + const zammadPaths = [ "/zammad", - "/api/v1", "/auth/sso", "/assets", "/mobile", @@ -39,28 +38,50 @@ const checkRewrites = async (request: NextRequestWithAuth) => { const email = token?.email?.toLowerCase() ?? "unknown"; let headers = { "x-forwarded-user": email }; - if (request.nextUrl.pathname.startsWith("/dashboards")) { - const roles: string[] = (token?.roles as string[]) ?? []; - const leafcutterRole = roles.includes("admin") - ? "leafcutter_admin" - : "leafcutter_user"; - headers["x-forwarded-roles"] = leafcutterRole; - // headers["secruitytenant"] = "global"; - // headers["x-forwarded-for"] = 'link'; - - return rewriteURL( - request, - `${linkBaseURL}/dashboards`, - opensearchDashboardsURL, - headers, - ); - } else if (request.nextUrl.pathname.startsWith("/zammad")) { + if (request.nextUrl.pathname.startsWith("/zammad")) { return rewriteURL(request, `${linkBaseURL}/zammad`, zammadURL, headers); } else if (zammadPaths.some((p) => request.nextUrl.pathname.startsWith(p))) { return rewriteURL(request, linkBaseURL, zammadURL, headers); - } + } else { + const isDev = process.env.NODE_ENV === "development"; + const nonce = Buffer.from(crypto.randomUUID()).toString("base64"); + const cspHeader = ` + default-src 'self'; + connect-src 'self'; + script-src 'self' 'nonce-${nonce}' 'strict-dynamic' ${isDev ? "'unsafe-eval'" : ""}; + style-src 'self' 'unsafe-inline'; + img-src 'self' blob: data:; + font-src 'self'; + object-src 'none'; + base-uri 'self'; + form-action 'self'; + frame-ancestors 'none'; + upgrade-insecure-requests; +`; + const contentSecurityPolicyHeaderValue = cspHeader + .replace(/\s{2,}/g, " ") + .trim(); - return NextResponse.next(); + const requestHeaders = new Headers(request.headers); + requestHeaders.set("x-nonce", nonce); + requestHeaders.set( + "Content-Security-Policy", + contentSecurityPolicyHeaderValue, + ); + + const response = NextResponse.next({ + request: { + headers: requestHeaders, + }, + }); + + response.headers.set( + "Content-Security-Policy", + contentSecurityPolicyHeaderValue, + ); + + return response; + } }; export default withAuth(checkRewrites, { @@ -69,12 +90,11 @@ export default withAuth(checkRewrites, { }, callbacks: { authorized: ({ token, req }) => { - const path = req.nextUrl.pathname; - if (process.env.SETUP_MODE === "true") { return true; } + const path = req.nextUrl.pathname; const roles: any = token?.roles ?? []; if (path.startsWith("/admin") && !roles.includes("admin")) { @@ -91,7 +111,9 @@ export default withAuth(checkRewrites, { }); export const config = { - matcher: [ - "/((?!ws|wss|api/v1|api/signal|api/whatsapp|api/facebook|_next/static|_next/image|favicon.ico).*)", + matcher: ["/((?!ws|wss|api|_next/static|_next/image|favicon.ico).*)"], + missing: [ + { type: "header", key: "next-router-prefetch" }, + { type: "header", key: "purpose", value: "prefetch" }, ], }; diff --git a/apps/link/next.config.js b/apps/link/next.config.js index e74f81e..a3f1c8f 100644 --- a/apps/link/next.config.js +++ b/apps/link/next.config.js @@ -8,22 +8,37 @@ const nextConfig = { "@link-stack/bridge-ui", "mui-chips-input", ], - publicRuntimeConfig: { - linkURL: process.env.LINK_URL ?? "http://localhost:3000", - bridgeURL: process.env.BRIDGE_URL ?? "http://localhost:8002", - labelStudioURL: process.env.LABEL_STUDIO_URL ?? "http://localhost:8006", - muiLicenseKey: process.env.MUI_LICENSE_KEY ?? "", + headers: async () => { + return [ + { + source: "/((?!zammad).*)", + headers: [ + { + key: "Strict-Transport-Security", + value: "max-age=63072000; includeSubDomains; preload", + }, + { + key: "X-Frame-Options", + value: "DENY", + }, + { + key: "X-Content-Type-Options", + value: "nosniff", + }, + ], + }, + ]; }, rewrites: async () => { return { beforeFiles: [ { source: "/api/v1/:path*", - destination: `http://zammad-nginx:8080/api/v1/:path*`, + destination: `${process.env.ZAMMAD_URL ?? "http://zammad-nginx:8080"}/api/v1/:path*`, }, { source: "/ws", - destination: `http://zammad-nginx:8080/ws`, + destination: `${process.env.ZAMMAD_URL ?? "http://zammad-nginx:8080"}/ws`, }, ], };