diff --git a/.env.template b/.env.template index 8969789f..6fdad6e9 100644 --- a/.env.template +++ b/.env.template @@ -32,3 +32,7 @@ FEEDBACK_URL_LINK= # frame-ancestors attribute of CSP. Separate multiple values with a space FRAME_ANCESTORS= + +# Allowed CORS origins (comma-separated). Example: https://app.example.com,https://admin.example.com +# Leave empty to use POST_LOGIN_REDIRECT as the default allowed origin +ALLOWED_CORS_ORIGINS= diff --git a/package-lock.json b/package-lock.json index fff9284e..17125a58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@apollo/client": "3.14.0", "@fastify/autoload": "6.3.1", "@fastify/cookie": "11.0.2", + "@fastify/cors": "^11.1.0", "@fastify/env": "5.0.3", "@fastify/helmet": "13.0.2", "@fastify/http-proxy": "11.3.0", @@ -1434,6 +1435,26 @@ "fastify-plugin": "^5.0.0" } }, + "node_modules/@fastify/cors": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-11.1.0.tgz", + "integrity": "sha512-sUw8ed8wP2SouWZTIbA7V2OQtMNpLj2W6qJOYhNdcmINTu6gsxVYXjQiM9mdi8UUDlcoDDJ/W2syPo1WB2QjYA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fastify-plugin": "^5.0.0", + "toad-cache": "^3.7.0" + } + }, "node_modules/@fastify/deepmerge": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@fastify/deepmerge/-/deepmerge-3.1.0.tgz", diff --git a/package.json b/package.json index 8d796074..4b76703c 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@apollo/client": "3.14.0", "@fastify/autoload": "6.3.1", "@fastify/cookie": "11.0.2", + "@fastify/cors": "11.1.0", "@fastify/env": "5.0.3", "@fastify/helmet": "13.0.2", "@fastify/http-proxy": "11.3.0", diff --git a/server.ts b/server.ts index 9c025a82..30e9e6b5 100644 --- a/server.ts +++ b/server.ts @@ -1,5 +1,6 @@ import Fastify from 'fastify'; import FastifyVite from '@fastify/vite'; +import cors from '@fastify/cors'; import helmet from '@fastify/helmet'; import { fileURLToPath } from 'node:url'; import path from 'node:path'; @@ -12,8 +13,6 @@ import { injectDynatraceTag } from './server/config/dynatrace.js'; dotenv.config(); -console.log(process.env); - const { DYNATRACE_SCRIPT_URL } = process.env; if (DYNATRACE_SCRIPT_URL) { injectDynatraceTag(DYNATRACE_SCRIPT_URL); @@ -67,6 +66,28 @@ const fastify = Fastify({ logger: true, }); +fastify.register(cors, { + origin: isLocalDev + ? true // Allow all origins in local development + : (origin, callback) => { + // In production, validate against allowed origins + // @ts-ignore + const allowedOrigins = fastify.config.ALLOWED_CORS_ORIGINS + ? // @ts-ignore + fastify.config.ALLOWED_CORS_ORIGINS.split(',').map((o) => o.trim()) + : // @ts-ignore + [fastify.config.POST_LOGIN_REDIRECT]; // Fallback to POST_LOGIN_REDIRECT + + if (!origin || allowedOrigins.includes(origin)) { + callback(null, true); + } else { + callback(new Error(`Origin ${origin} not allowed by CORS policy`), false); + } + }, + methods: ['GET', 'HEAD', 'POST', 'PATCH', 'DELETE'], + credentials: true, // Required for cookie-based sessions +}); + Sentry.setupFastifyErrorHandler(fastify); await fastify.register(envPlugin); @@ -94,6 +115,9 @@ if (DYNATRACE_SCRIPT_URL) { fastify.register(helmet, { contentSecurityPolicy: { directives: { + defaultSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'"], + imgSrc: ["'self'", 'data:', 'https:'], 'connect-src': ["'self'", 'sdk.openui5.org', sentryHost, dynatraceOrigin], 'script-src': isLocalDev ? ["'self'", "'unsafe-inline'", "'unsafe-eval'", sentryHost, dynatraceOrigin] @@ -102,6 +126,12 @@ fastify.register(helmet, { 'frame-ancestors': [...fastify.config.FRAME_ANCESTORS.split(',')], }, }, + // Needed for https enforcement + hsts: { + maxAge: 31536000, + includeSubDomains: true, + preload: true, + }, }); fastify.register(proxy, { diff --git a/server/config/env.ts b/server/config/env.ts index 1bcfe43a..b495579d 100644 --- a/server/config/env.ts +++ b/server/config/env.ts @@ -29,6 +29,7 @@ const schema = { FEEDBACK_SLACK_URL: { type: 'string' }, FEEDBACK_URL_LINK: { type: 'string' }, FRAME_ANCESTORS: { type: 'string' }, + ALLOWED_CORS_ORIGINS: { type: 'string' }, BFF_SENTRY_DSN: { type: 'string' }, FRONTEND_SENTRY_DSN: { type: 'string' }, FRONTEND_SENTRY_ENVIRONMENT: { type: 'string' }, diff --git a/vite.config.js b/vite.config.js index 283c2549..6d8e139d 100644 --- a/vite.config.js +++ b/vite.config.js @@ -35,7 +35,7 @@ export default defineConfig({ }, build: { - sourcemap: true, + sourcemap: process.env.NODE_ENV !== 'production', target: 'esnext', // Support top-level await }, });