From 5513eb88412482a52efad9eed05a6ad137a85aa8 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Fri, 13 Jun 2025 09:06:54 +0200 Subject: [PATCH] custom header in middleware --- apps/login/src/i18n/request.ts | 14 +++++++++ apps/login/src/lib/zitadel.ts | 27 ++++++++++++++++ apps/login/src/middleware.ts | 49 ++++++++++++++++++++++------- packages/zitadel-proto/package.json | 2 +- 4 files changed, 80 insertions(+), 12 deletions(-) diff --git a/apps/login/src/i18n/request.ts b/apps/login/src/i18n/request.ts index 59c9da42cc..71ccaebae5 100644 --- a/apps/login/src/i18n/request.ts +++ b/apps/login/src/i18n/request.ts @@ -1,4 +1,6 @@ import { LANGS, LANGUAGE_COOKIE_NAME, LANGUAGE_HEADER_NAME } from "@/lib/i18n"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; +import { getHostedLoginTranslation } from "@/lib/zitadel"; import deepmerge from "deepmerge"; import { getRequestConfig } from "next-intl/server"; import { cookies, headers } from "next/headers"; @@ -9,6 +11,18 @@ export default getRequestConfig(async () => { let locale: string = fallback; + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + const i18nOrganization = _headers.get("x-zitadel-i18n-organization") || ""; // You may need to set this header in middleware + + const translations = await getHostedLoginTranslation({ + serviceUrl, + organization: i18nOrganization, + }); + + translations. + const languageHeader = await (await headers()).get(LANGUAGE_HEADER_NAME); if (languageHeader) { const headerLocale = languageHeader.split(",")[0].split("-")[0]; // Extract the language code diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts index d5045df041..f567350053 100644 --- a/apps/login/src/lib/zitadel.ts +++ b/apps/login/src/lib/zitadel.ts @@ -21,6 +21,7 @@ import { SessionService, } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import { TranslationLevelType } from "@zitadel/proto/zitadel/settings/v2/settings_pb"; import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb"; import { SendEmailVerificationCodeSchema } from "@zitadel/proto/zitadel/user/v2/email_pb"; import type { RedirectURLsJson } from "@zitadel/proto/zitadel/user/v2/idp_pb"; @@ -45,6 +46,7 @@ import { VerifyPasskeyRegistrationRequest, VerifyU2FRegistrationRequest, } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import { getLocale } from "next-intl/server"; import { unstable_cacheLife as cacheLife } from "next/cache"; import { getUserAgent } from "./fingerprint"; import { createServiceForHost } from "./service"; @@ -58,6 +60,31 @@ async function cacheWrapper(callback: Promise) { return callback; } +export async function getHostedLoginTranslation({ + serviceUrl, + organization, +}: { + serviceUrl: string; + organization?: string; +}) { + const locale = await getLocale(); + const settingsService: Client = + await createServiceForHost(SettingsService, serviceUrl); + + const callback = settingsService + .getHostedLoginTranslation( + { + level: TranslationLevelType.INSTANCE, + levelId: organization, + locale: locale, + }, + {}, + ) + .then((resp) => (resp.translations ? resp.translations : undefined)); + + return useCache ? cacheWrapper(callback) : callback; +} + export async function getBrandingSettings({ serviceUrl, organization, diff --git a/apps/login/src/middleware.ts b/apps/login/src/middleware.ts index 4d66d0ab39..5c04a26d13 100644 --- a/apps/login/src/middleware.ts +++ b/apps/login/src/middleware.ts @@ -10,21 +10,52 @@ export const config = { "/oidc/:path*", "/idps/callback/:path*", "/saml/:path*", + // Add "/*" to match all routes for translation header injection + "/*", ], }; export async function middleware(request: NextRequest) { + // Add the original URL as a header to all requests + const requestHeaders = new Headers(request.headers); + + // Extract "organization" search param from the URL and set it as a header if available + const organization = request.nextUrl.searchParams.get("organization"); + if (organization) { + requestHeaders.set("x-zitadel-i18n-organization", organization); + } + + // Only run the rest of the logic for the original matcher paths + const matchedPaths = [ + "/.well-known/", + "/oauth/", + "/oidc/", + "/idps/callback/", + "/saml/", + ]; + + const isMatched = matchedPaths.some((prefix) => + request.nextUrl.pathname.startsWith(prefix), + ); + + if (!isMatched) { + // For all other routes, just add the header and continue + return NextResponse.next({ + request: { headers: requestHeaders }, + }); + } + // escape proxy if the environment is setup for multitenancy if (!process.env.ZITADEL_API_URL || !process.env.ZITADEL_SERVICE_USER_TOKEN) { - return NextResponse.next(); + return NextResponse.next({ + request: { headers: requestHeaders }, + }); } const _headers = await headers(); - const { serviceUrl } = getServiceUrlFromHeaders(_headers); // Call the /security route handler - // TODO check this on cloud run deployment const securityResponse = await fetch(`${request.nextUrl.origin}/security`); if (!securityResponse.ok) { @@ -32,7 +63,9 @@ export async function middleware(request: NextRequest) { "Failed to fetch security settings:", securityResponse.statusText, ); - return NextResponse.next(); // Fallback if the request fails + return NextResponse.next({ + request: { headers: requestHeaders }, + }); } const { settings: securitySettings } = await securityResponse.json(); @@ -41,13 +74,8 @@ export async function middleware(request: NextRequest) { .replace("https://", "") .replace("http://", ""); - const requestHeaders = new Headers(request.headers); - - // this is a workaround for the next.js server not forwarding the host header - // requestHeaders.set("x-zitadel-forwarded", `host="${request.nextUrl.host}"`); + // Add additional headers as before requestHeaders.set("x-zitadel-public-host", `${request.nextUrl.host}`); - - // this is a workaround for the next.js server not forwarding the host header requestHeaders.set("x-zitadel-instance-host", instanceHost); const responseHeaders = new Headers(); @@ -55,7 +83,6 @@ export async function middleware(request: NextRequest) { responseHeaders.set("Access-Control-Allow-Headers", "*"); if (securitySettings?.embeddedIframe?.enabled) { - securitySettings.embeddedIframe.allowedOrigins; responseHeaders.set( "Content-Security-Policy", `${DEFAULT_CSP} frame-ancestors ${securitySettings.embeddedIframe.allowedOrigins.join(" ")};`, diff --git a/packages/zitadel-proto/package.json b/packages/zitadel-proto/package.json index 61ef296616..2fd912f4df 100644 --- a/packages/zitadel-proto/package.json +++ b/packages/zitadel-proto/package.json @@ -14,7 +14,7 @@ ], "sideEffects": false, "scripts": { - "generate": "buf generate https://github.com/zitadel/zitadel.git --path ./proto/zitadel", + "generate": "buf generate https://github.com/zitadel/zitadel.git#branch=feat/9850-hosted-login-translation-api --path ./proto/zitadel", "clean": "rm -rf zitadel .turbo node_modules google protoc-gen-openapiv2 validate" }, "dependencies": {