diff --git a/apps/login/next.config.mjs b/apps/login/next.config.mjs index 00fa1e19c4..2795854114 100755 --- a/apps/login/next.config.mjs +++ b/apps/login/next.config.mjs @@ -1,4 +1,5 @@ import createNextIntlPlugin from "next-intl/plugin"; +import { DEFAULT_CSP } from "./src/lib/csp"; const withNextIntl = createNextIntlPlugin(); @@ -29,9 +30,9 @@ const secureHeaders = [ // script-src va.vercel-scripts.com for analytics/vercel scripts { key: "Content-Security-Policy", - value: - "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://va.vercel-scripts.com; connect-src 'self'; child-src; style-src 'self' 'unsafe-inline'; font-src 'self'; object-src 'none'; img-src 'self' https://vercel.com;", + value: `${DEFAULT_CSP} frame-ancestors 'none'`, }, + { key: "X-Frame-Options", value: "deny" }, ]; const imageRemotePatterns = [ diff --git a/apps/login/src/app/(login)/authenticator/set/page.tsx b/apps/login/src/app/(login)/authenticator/set/page.tsx index 8240023c2d..8904eff963 100644 --- a/apps/login/src/app/(login)/authenticator/set/page.tsx +++ b/apps/login/src/app/(login)/authenticator/set/page.tsx @@ -33,8 +33,8 @@ export default async function Page(props: { const { serviceUrl } = getServiceUrlFromHeaders(_headers); const sessionWithData = sessionId - ? await loadSessionById(serviceUrl, sessionId, organization) - : await loadSessionByLoginname(serviceUrl, loginName, organization); + ? await loadSessionById(sessionId, organization) + : await loadSessionByLoginname(loginName, organization); async function getAuthMethodsAndUser( serviceUrl: string, @@ -67,7 +67,6 @@ export default async function Page(props: { } async function loadSessionByLoginname( - host: string, loginName?: string, organization?: string, ) { @@ -82,11 +81,7 @@ export default async function Page(props: { }); } - async function loadSessionById( - host: string, - sessionId: string, - organization?: string, - ) { + async function loadSessionById(sessionId: string, organization?: string) { const recent = await getSessionCookieById({ sessionId, organization }); return getSession({ serviceUrl, diff --git a/apps/login/src/app/login/route.ts b/apps/login/src/app/login/route.ts index e3834e5a27..b3da6f863e 100644 --- a/apps/login/src/app/login/route.ts +++ b/apps/login/src/app/login/route.ts @@ -1,4 +1,5 @@ import { getAllSessions } from "@/lib/cookies"; +import { DEFAULT_CSP } from "@/lib/csp"; import { idpTypeToSlug } from "@/lib/idp"; import { loginWithOIDCandSession } from "@/lib/oidc"; import { loginWithSAMLandSession } from "@/lib/saml"; @@ -12,6 +13,7 @@ import { getAuthRequest, getOrgsByDomain, getSAMLRequest, + getSecuritySettings, listSessions, startIdentityProviderFlow, } from "@/lib/zitadel"; @@ -293,17 +295,32 @@ export async function GET(request: NextRequest) { * This means that the user should not be prompted to enter their password again. * Instead, the server attempts to silently authenticate the user using an existing session or other authentication mechanisms that do not require user interaction **/ + const securitySettings = await getSecuritySettings({ + serviceUrl, + }); + const selectedSession = await findValidSession({ serviceUrl, sessions, authRequest, }); - if (!selectedSession || !selectedSession.id) { - return NextResponse.json( - { error: "No active session found" }, - { status: 400 }, + const noSessionResponse = NextResponse.json( + { error: "No active session found" }, + { status: 400 }, + ); + + if (securitySettings?.embeddedIframe?.enabled) { + securitySettings.embeddedIframe.allowedOrigins; + noSessionResponse.headers.set( + "Content-Security-Policy", + `${DEFAULT_CSP} frame-ancestors ${securitySettings.embeddedIframe.allowedOrigins.join(" ")};`, ); + noSessionResponse.headers.delete("X-Frame-Options"); + } + + if (!selectedSession || !selectedSession.id) { + return noSessionResponse; } const cookie = sessionCookies.find( @@ -311,10 +328,7 @@ export async function GET(request: NextRequest) { ); if (!cookie || !cookie.id || !cookie.token) { - return NextResponse.json( - { error: "No active session found" }, - { status: 400 }, - ); + return noSessionResponse; } const session = { @@ -332,7 +346,19 @@ export async function GET(request: NextRequest) { }, }), }); - return NextResponse.redirect(callbackUrl); + + const callbackResponse = NextResponse.redirect(callbackUrl); + + if (securitySettings?.embeddedIframe?.enabled) { + securitySettings.embeddedIframe.allowedOrigins; + callbackResponse.headers.set( + "Content-Security-Policy", + `${DEFAULT_CSP} frame-ancestors ${securitySettings.embeddedIframe.allowedOrigins.join(" ")};`, + ); + callbackResponse.headers.delete("X-Frame-Options"); + } + + return callbackResponse; } else { // check for loginHint, userId hint and valid sessions let selectedSession = await findValidSession({ diff --git a/apps/login/src/lib/csp.ts b/apps/login/src/lib/csp.ts new file mode 100644 index 0000000000..5cc1e254f3 --- /dev/null +++ b/apps/login/src/lib/csp.ts @@ -0,0 +1,2 @@ +export const DEFAULT_CSP = + "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://va.vercel-scripts.com; connect-src 'self'; child-src; style-src 'self' 'unsafe-inline'; font-src 'self'; object-src 'none'; img-src 'self' https://vercel.com;"; diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts index 0511eaaf0d..a5abc0dcb1 100644 --- a/apps/login/src/lib/zitadel.ts +++ b/apps/login/src/lib/zitadel.ts @@ -92,6 +92,21 @@ export async function getLoginSettings({ return useCache ? cacheWrapper(callback) : callback; } +export async function getSecuritySettings({ + serviceUrl, +}: { + serviceUrl: string; +}) { + const settingsService: Client = + await createServiceForHost(SettingsService, serviceUrl); + + const callback = settingsService + .getSecuritySettings({}) + .then((resp) => (resp.settings ? resp.settings : undefined)); + + return useCache ? cacheWrapper(callback) : callback; +} + export async function getLockoutSettings({ serviceUrl, orgId, diff --git a/apps/login/src/middleware.ts b/apps/login/src/middleware.ts index 8d4080cddf..0572fe43b6 100644 --- a/apps/login/src/middleware.ts +++ b/apps/login/src/middleware.ts @@ -1,6 +1,8 @@ import { headers } from "next/headers"; import { NextRequest, NextResponse } from "next/server"; +import { DEFAULT_CSP } from "./lib/csp"; import { getServiceUrlFromHeaders } from "./lib/service"; +import { getSecuritySettings } from "./lib/zitadel"; export const config = { matcher: [ @@ -22,6 +24,8 @@ export async function middleware(request: NextRequest) { const { serviceUrl } = getServiceUrlFromHeaders(_headers); + const securitySettings = await getSecuritySettings({ serviceUrl }); + const instanceHost = `${serviceUrl}` .replace("https://", "") .replace("http://", ""); @@ -39,6 +43,20 @@ export async function middleware(request: NextRequest) { responseHeaders.set("Access-Control-Allow-Origin", "*"); responseHeaders.set("Access-Control-Allow-Headers", "*"); + responseHeaders.set( + "Content-Security-Policy", + `${DEFAULT_CSP} frame-ancestors 'none'`, + ); + + if (securitySettings?.embeddedIframe?.enabled) { + securitySettings.embeddedIframe.allowedOrigins; + responseHeaders.set( + "Content-Security-Policy", + `${DEFAULT_CSP} frame-ancestors ${securitySettings.embeddedIframe.allowedOrigins.join(" ")};`, + ); + responseHeaders.delete("X-Frame-Options"); + } + request.nextUrl.href = `${serviceUrl}${request.nextUrl.pathname}${request.nextUrl.search}`; return NextResponse.rewrite(request.nextUrl, { request: {