Merge pull request #445 from zitadel/qa

Promote qa to prod: iframe options, fix middleware for edge runtime
This commit is contained in:
Max Peintner
2025-05-07 10:40:57 +02:00
committed by GitHub
52 changed files with 359 additions and 189 deletions

View File

@@ -4,6 +4,7 @@ on:
push: push:
branches: branches:
- main - main
- qa
workflow_dispatch: workflow_dispatch:
permissions: permissions:
@@ -41,7 +42,7 @@ jobs:
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
with: with:
driver-opts: 'image=moby/buildkit:v0.11.6' driver: docker-container
- name: Login Public - name: Login Public
uses: docker/login-action@v3 uses: docker/login-action@v3

View File

@@ -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;";

View File

@@ -6,6 +6,13 @@
"data": {} "data": {}
} }
}, },
{
"service": "zitadel.settings.v2.SettingsService",
"method": "GetSecuritySettings",
"out": {
"data": {}
}
},
{ {
"service": "zitadel.settings.v2.SettingsService", "service": "zitadel.settings.v2.SettingsService",
"method": "GetLegalAndSupportSettings", "method": "GetLegalAndSupportSettings",

View File

@@ -1,4 +1,5 @@
import createNextIntlPlugin from "next-intl/plugin"; import createNextIntlPlugin from "next-intl/plugin";
import { DEFAULT_CSP } from "./constants/csp.js";
const withNextIntl = createNextIntlPlugin(); const withNextIntl = createNextIntlPlugin();
@@ -29,9 +30,9 @@ const secureHeaders = [
// script-src va.vercel-scripts.com for analytics/vercel scripts // script-src va.vercel-scripts.com for analytics/vercel scripts
{ {
key: "Content-Security-Policy", key: "Content-Security-Policy",
value: value: `${DEFAULT_CSP} frame-ancestors 'none'`,
"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;",
}, },
{ key: "X-Frame-Options", value: "deny" },
]; ];
const imageRemotePatterns = [ const imageRemotePatterns = [

View File

@@ -1,7 +1,7 @@
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { SessionsList } from "@/components/sessions-list"; import { SessionsList } from "@/components/sessions-list";
import { getAllSessionCookieIds } from "@/lib/cookies"; import { getAllSessionCookieIds } from "@/lib/cookies";
import { getServiceUrlFromHeaders } from "@/lib/service"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { import {
getBrandingSettings, getBrandingSettings,
getDefaultOrg, getDefaultOrg,

View File

@@ -5,7 +5,7 @@ import { DynamicTheme } from "@/components/dynamic-theme";
import { SignInWithIdp } from "@/components/sign-in-with-idp"; import { SignInWithIdp } from "@/components/sign-in-with-idp";
import { UserAvatar } from "@/components/user-avatar"; import { UserAvatar } from "@/components/user-avatar";
import { getSessionCookieById } from "@/lib/cookies"; import { getSessionCookieById } from "@/lib/cookies";
import { getServiceUrlFromHeaders } from "@/lib/service"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session"; import { loadMostRecentSession } from "@/lib/session";
import { import {
getActiveIdentityProviders, getActiveIdentityProviders,
@@ -33,8 +33,8 @@ export default async function Page(props: {
const { serviceUrl } = getServiceUrlFromHeaders(_headers); const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const sessionWithData = sessionId const sessionWithData = sessionId
? await loadSessionById(serviceUrl, sessionId, organization) ? await loadSessionById(sessionId, organization)
: await loadSessionByLoginname(serviceUrl, loginName, organization); : await loadSessionByLoginname(loginName, organization);
async function getAuthMethodsAndUser( async function getAuthMethodsAndUser(
serviceUrl: string, serviceUrl: string,
@@ -67,7 +67,6 @@ export default async function Page(props: {
} }
async function loadSessionByLoginname( async function loadSessionByLoginname(
host: string,
loginName?: string, loginName?: string,
organization?: string, organization?: string,
) { ) {
@@ -82,11 +81,7 @@ export default async function Page(props: {
}); });
} }
async function loadSessionById( async function loadSessionById(sessionId: string, organization?: string) {
host: string,
sessionId: string,
organization?: string,
) {
const recent = await getSessionCookieById({ sessionId, organization }); const recent = await getSessionCookieById({ sessionId, organization });
return getSession({ return getSession({
serviceUrl, serviceUrl,

View File

@@ -1,6 +1,6 @@
import { ConsentScreen } from "@/components/consent"; import { ConsentScreen } from "@/components/consent";
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { getServiceUrlFromHeaders } from "@/lib/service"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { import {
getBrandingSettings, getBrandingSettings,
getDefaultOrg, getDefaultOrg,

View File

@@ -1,6 +1,6 @@
import { DeviceCodeForm } from "@/components/device-code-form"; import { DeviceCodeForm } from "@/components/device-code-form";
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { getServiceUrlFromHeaders } from "@/lib/service"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { getBrandingSettings, getDefaultOrg } from "@/lib/zitadel"; import { getBrandingSettings, getDefaultOrg } from "@/lib/zitadel";
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
import { getLocale, getTranslations } from "next-intl/server"; import { getLocale, getTranslations } from "next-intl/server";

View File

@@ -2,7 +2,7 @@ import { Alert, AlertType } from "@/components/alert";
import { ChooseAuthenticatorToLogin } from "@/components/choose-authenticator-to-login"; import { ChooseAuthenticatorToLogin } from "@/components/choose-authenticator-to-login";
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { UserAvatar } from "@/components/user-avatar"; import { UserAvatar } from "@/components/user-avatar";
import { getServiceUrlFromHeaders } from "@/lib/service"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { import {
getBrandingSettings, getBrandingSettings,
getLoginSettings, getLoginSettings,

View File

@@ -4,8 +4,7 @@ import { linkingFailed } from "@/components/idps/pages/linking-failed";
import { linkingSuccess } from "@/components/idps/pages/linking-success"; import { linkingSuccess } from "@/components/idps/pages/linking-success";
import { loginFailed } from "@/components/idps/pages/login-failed"; import { loginFailed } from "@/components/idps/pages/login-failed";
import { loginSuccess } from "@/components/idps/pages/login-success"; import { loginSuccess } from "@/components/idps/pages/login-success";
import { idpTypeToIdentityProviderType } from "@/lib/idp"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { getServiceUrlFromHeaders } from "@/lib/service";
import { import {
addHuman, addHuman,
addIDPLink, addIDPLink,
@@ -16,10 +15,13 @@ import {
listUsers, listUsers,
retrieveIDPIntent, retrieveIDPIntent,
} from "@/lib/zitadel"; } from "@/lib/zitadel";
import { create } from "@zitadel/client"; import { ConnectError, create } from "@zitadel/client";
import { AutoLinkingOption } from "@zitadel/proto/zitadel/idp/v2/idp_pb"; import { AutoLinkingOption } from "@zitadel/proto/zitadel/idp/v2/idp_pb";
import { OrganizationSchema } from "@zitadel/proto/zitadel/object/v2/object_pb"; import { OrganizationSchema } from "@zitadel/proto/zitadel/object/v2/object_pb";
import { AddHumanUserRequestSchema } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import {
AddHumanUserRequest,
AddHumanUserRequestSchema,
} from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { getLocale, getTranslations } from "next-intl/server"; import { getLocale, getTranslations } from "next-intl/server";
import { headers } from "next/headers"; import { headers } from "next/headers";
@@ -83,8 +85,6 @@ export default async function Page(props: {
throw new Error("IDP not found"); throw new Error("IDP not found");
} }
const providerType = idpTypeToIdentityProviderType(idp.type);
if (link) { if (link) {
if (!options?.isLinkingAllowed) { if (!options?.isLinkingAllowed) {
// linking was probably disallowed since the invitation was created // linking was probably disallowed since the invitation was created
@@ -205,20 +205,42 @@ export default async function Page(props: {
} }
} }
if (addHumanUser && orgToRegisterOn) { if (addHumanUser) {
let addHumanUserWithOrganization: AddHumanUserRequest;
if (orgToRegisterOn) {
const organizationSchema = create(OrganizationSchema, { const organizationSchema = create(OrganizationSchema, {
org: { case: "orgId", value: orgToRegisterOn }, org: { case: "orgId", value: orgToRegisterOn },
}); });
const addHumanUserWithOrganization = create(AddHumanUserRequestSchema, { addHumanUserWithOrganization = create(AddHumanUserRequestSchema, {
...addHumanUser, ...addHumanUser,
organization: organizationSchema, organization: organizationSchema,
}); });
} else {
addHumanUserWithOrganization = create(
AddHumanUserRequestSchema,
addHumanUser,
);
}
try {
newUser = await addHuman({ newUser = await addHuman({
serviceUrl, serviceUrl,
request: addHumanUserWithOrganization, request: addHumanUserWithOrganization,
}); });
} catch (error: unknown) {
console.error(
"An error occurred while creating the user:",
error,
addHumanUser,
);
return loginFailed(
branding,
(error as ConnectError).message
? (error as ConnectError).message
: "Could not create user",
);
}
} }
if (newUser) { if (newUser) {

View File

@@ -1,6 +1,6 @@
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { SignInWithIdp } from "@/components/sign-in-with-idp"; import { SignInWithIdp } from "@/components/sign-in-with-idp";
import { getServiceUrlFromHeaders } from "@/lib/service"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { getActiveIdentityProviders, getBrandingSettings } from "@/lib/zitadel"; import { getActiveIdentityProviders, getBrandingSettings } from "@/lib/zitadel";
import { getLocale, getTranslations } from "next-intl/server"; import { getLocale, getTranslations } from "next-intl/server";
import { headers } from "next/headers"; import { headers } from "next/headers";

View File

@@ -1,7 +1,7 @@
import { Alert, AlertType } from "@/components/alert"; import { Alert, AlertType } from "@/components/alert";
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { InviteForm } from "@/components/invite-form"; import { InviteForm } from "@/components/invite-form";
import { getServiceUrlFromHeaders } from "@/lib/service"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { import {
getBrandingSettings, getBrandingSettings,
getDefaultOrg, getDefaultOrg,

View File

@@ -2,7 +2,7 @@ import { Alert, AlertType } from "@/components/alert";
import { Button, ButtonVariants } from "@/components/button"; import { Button, ButtonVariants } from "@/components/button";
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { UserAvatar } from "@/components/user-avatar"; import { UserAvatar } from "@/components/user-avatar";
import { getServiceUrlFromHeaders } from "@/lib/service"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { getBrandingSettings, getDefaultOrg, getUserByID } from "@/lib/zitadel"; import { getBrandingSettings, getDefaultOrg, getUserByID } from "@/lib/zitadel";
import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb";
import { getLocale, getTranslations } from "next-intl/server"; import { getLocale, getTranslations } from "next-intl/server";

View File

@@ -1,7 +1,7 @@
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { SignInWithIdp } from "@/components/sign-in-with-idp"; import { SignInWithIdp } from "@/components/sign-in-with-idp";
import { UsernameForm } from "@/components/username-form"; import { UsernameForm } from "@/components/username-form";
import { getServiceUrlFromHeaders } from "@/lib/service"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { import {
getActiveIdentityProviders, getActiveIdentityProviders,
getBrandingSettings, getBrandingSettings,

View File

@@ -4,7 +4,7 @@ import { ChooseSecondFactor } from "@/components/choose-second-factor";
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { UserAvatar } from "@/components/user-avatar"; import { UserAvatar } from "@/components/user-avatar";
import { getSessionCookieById } from "@/lib/cookies"; import { getSessionCookieById } from "@/lib/cookies";
import { getServiceUrlFromHeaders } from "@/lib/service"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session"; import { loadMostRecentSession } from "@/lib/session";
import { import {
getBrandingSettings, getBrandingSettings,

View File

@@ -4,7 +4,7 @@ import { ChooseSecondFactorToSetup } from "@/components/choose-second-factor-to-
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { UserAvatar } from "@/components/user-avatar"; import { UserAvatar } from "@/components/user-avatar";
import { getSessionCookieById } from "@/lib/cookies"; import { getSessionCookieById } from "@/lib/cookies";
import { getServiceUrlFromHeaders } from "@/lib/service"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session"; import { loadMostRecentSession } from "@/lib/session";
import { import {
getBrandingSettings, getBrandingSettings,

View File

@@ -3,7 +3,7 @@ import { DynamicTheme } from "@/components/dynamic-theme";
import { LoginOTP } from "@/components/login-otp"; import { LoginOTP } from "@/components/login-otp";
import { UserAvatar } from "@/components/user-avatar"; import { UserAvatar } from "@/components/user-avatar";
import { getSessionCookieById } from "@/lib/cookies"; import { getSessionCookieById } from "@/lib/cookies";
import { getServiceUrlFromHeaders } from "@/lib/service"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session"; import { loadMostRecentSession } from "@/lib/session";
import { import {
getBrandingSettings, getBrandingSettings,

View File

@@ -4,7 +4,7 @@ import { Button, ButtonVariants } from "@/components/button";
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { TotpRegister } from "@/components/totp-register"; import { TotpRegister } from "@/components/totp-register";
import { UserAvatar } from "@/components/user-avatar"; import { UserAvatar } from "@/components/user-avatar";
import { getServiceUrlFromHeaders } from "@/lib/service"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session"; import { loadMostRecentSession } from "@/lib/session";
import { import {
addOTPEmail, addOTPEmail,

View File

@@ -3,7 +3,7 @@ import { DynamicTheme } from "@/components/dynamic-theme";
import { LoginPasskey } from "@/components/login-passkey"; import { LoginPasskey } from "@/components/login-passkey";
import { UserAvatar } from "@/components/user-avatar"; import { UserAvatar } from "@/components/user-avatar";
import { getSessionCookieById } from "@/lib/cookies"; import { getSessionCookieById } from "@/lib/cookies";
import { getServiceUrlFromHeaders } from "@/lib/service"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session"; import { loadMostRecentSession } from "@/lib/session";
import { getBrandingSettings, getSession } from "@/lib/zitadel"; import { getBrandingSettings, getSession } from "@/lib/zitadel";
import { getLocale, getTranslations } from "next-intl/server"; import { getLocale, getTranslations } from "next-intl/server";

View File

@@ -2,7 +2,7 @@ import { Alert, AlertType } from "@/components/alert";
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { RegisterPasskey } from "@/components/register-passkey"; import { RegisterPasskey } from "@/components/register-passkey";
import { UserAvatar } from "@/components/user-avatar"; import { UserAvatar } from "@/components/user-avatar";
import { getServiceUrlFromHeaders } from "@/lib/service"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session"; import { loadMostRecentSession } from "@/lib/session";
import { getBrandingSettings } from "@/lib/zitadel"; import { getBrandingSettings } from "@/lib/zitadel";
import { getLocale, getTranslations } from "next-intl/server"; import { getLocale, getTranslations } from "next-intl/server";

View File

@@ -2,7 +2,7 @@ import { Alert } from "@/components/alert";
import { ChangePasswordForm } from "@/components/change-password-form"; import { ChangePasswordForm } from "@/components/change-password-form";
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { UserAvatar } from "@/components/user-avatar"; import { UserAvatar } from "@/components/user-avatar";
import { getServiceUrlFromHeaders } from "@/lib/service"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session"; import { loadMostRecentSession } from "@/lib/session";
import { import {
getBrandingSettings, getBrandingSettings,

View File

@@ -2,7 +2,7 @@ import { Alert } from "@/components/alert";
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { PasswordForm } from "@/components/password-form"; import { PasswordForm } from "@/components/password-form";
import { UserAvatar } from "@/components/user-avatar"; import { UserAvatar } from "@/components/user-avatar";
import { getServiceUrlFromHeaders } from "@/lib/service"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session"; import { loadMostRecentSession } from "@/lib/session";
import { import {
getBrandingSettings, getBrandingSettings,

View File

@@ -2,7 +2,7 @@ import { Alert, AlertType } from "@/components/alert";
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { SetPasswordForm } from "@/components/set-password-form"; import { SetPasswordForm } from "@/components/set-password-form";
import { UserAvatar } from "@/components/user-avatar"; import { UserAvatar } from "@/components/user-avatar";
import { getServiceUrlFromHeaders } from "@/lib/service"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session"; import { loadMostRecentSession } from "@/lib/session";
import { import {
getBrandingSettings, getBrandingSettings,

View File

@@ -1,6 +1,6 @@
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { RegisterForm } from "@/components/register-form"; import { RegisterForm } from "@/components/register-form";
import { getServiceUrlFromHeaders } from "@/lib/service"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { import {
getBrandingSettings, getBrandingSettings,
getDefaultOrg, getDefaultOrg,

View File

@@ -1,6 +1,6 @@
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { SetRegisterPasswordForm } from "@/components/set-register-password-form"; import { SetRegisterPasswordForm } from "@/components/set-register-password-form";
import { getServiceUrlFromHeaders } from "@/lib/service"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { import {
getBrandingSettings, getBrandingSettings,
getDefaultOrg, getDefaultOrg,

View File

@@ -7,7 +7,7 @@ import {
getSessionCookieById, getSessionCookieById,
} from "@/lib/cookies"; } from "@/lib/cookies";
import { completeDeviceAuthorization } from "@/lib/server/device"; import { completeDeviceAuthorization } from "@/lib/server/device";
import { getServiceUrlFromHeaders } from "@/lib/service"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session"; import { loadMostRecentSession } from "@/lib/session";
import { import {
getBrandingSettings, getBrandingSettings,

View File

@@ -3,7 +3,7 @@ import { DynamicTheme } from "@/components/dynamic-theme";
import { LoginPasskey } from "@/components/login-passkey"; import { LoginPasskey } from "@/components/login-passkey";
import { UserAvatar } from "@/components/user-avatar"; import { UserAvatar } from "@/components/user-avatar";
import { getSessionCookieById } from "@/lib/cookies"; import { getSessionCookieById } from "@/lib/cookies";
import { getServiceUrlFromHeaders } from "@/lib/service"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session"; import { loadMostRecentSession } from "@/lib/session";
import { getBrandingSettings, getSession } from "@/lib/zitadel"; import { getBrandingSettings, getSession } from "@/lib/zitadel";
import { getLocale, getTranslations } from "next-intl/server"; import { getLocale, getTranslations } from "next-intl/server";

View File

@@ -2,7 +2,7 @@ import { Alert } from "@/components/alert";
import { DynamicTheme } from "@/components/dynamic-theme"; import { DynamicTheme } from "@/components/dynamic-theme";
import { RegisterU2f } from "@/components/register-u2f"; import { RegisterU2f } from "@/components/register-u2f";
import { UserAvatar } from "@/components/user-avatar"; import { UserAvatar } from "@/components/user-avatar";
import { getServiceUrlFromHeaders } from "@/lib/service"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session"; import { loadMostRecentSession } from "@/lib/session";
import { getBrandingSettings } from "@/lib/zitadel"; import { getBrandingSettings } from "@/lib/zitadel";
import { getLocale, getTranslations } from "next-intl/server"; import { getLocale, getTranslations } from "next-intl/server";

View File

@@ -4,7 +4,7 @@ import { UserAvatar } from "@/components/user-avatar";
import { VerifyForm } from "@/components/verify-form"; import { VerifyForm } from "@/components/verify-form";
import { VerifyRedirectButton } from "@/components/verify-redirect-button"; import { VerifyRedirectButton } from "@/components/verify-redirect-button";
import { sendEmailCode } from "@/lib/server/verify"; import { sendEmailCode } from "@/lib/server/verify";
import { getServiceUrlFromHeaders } from "@/lib/service"; import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { loadMostRecentSession } from "@/lib/session"; import { loadMostRecentSession } from "@/lib/session";
import { import {
getBrandingSettings, getBrandingSettings,

View File

@@ -3,7 +3,7 @@ import { idpTypeToSlug } from "@/lib/idp";
import { loginWithOIDCAndSession } from "@/lib/oidc"; import { loginWithOIDCAndSession } from "@/lib/oidc";
import { loginWithSAMLAndSession } from "@/lib/saml"; import { loginWithSAMLAndSession } from "@/lib/saml";
import { sendLoginname, SendLoginnameCommand } from "@/lib/server/loginname"; import { sendLoginname, SendLoginnameCommand } from "@/lib/server/loginname";
import { constructUrl, getServiceUrlFromHeaders } from "@/lib/service"; import { constructUrl, getServiceUrlFromHeaders } from "@/lib/service-url";
import { findValidSession } from "@/lib/session"; import { findValidSession } from "@/lib/session";
import { import {
createCallback, createCallback,
@@ -12,6 +12,7 @@ import {
getAuthRequest, getAuthRequest,
getOrgsByDomain, getOrgsByDomain,
getSAMLRequest, getSAMLRequest,
getSecuritySettings,
listSessions, listSessions,
startIdentityProviderFlow, startIdentityProviderFlow,
} from "@/lib/zitadel"; } from "@/lib/zitadel";
@@ -25,6 +26,7 @@ import { CreateResponseRequestSchema } from "@zitadel/proto/zitadel/saml/v2/saml
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { headers } from "next/headers"; import { headers } from "next/headers";
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { DEFAULT_CSP } from "../../../constants/csp";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
export const revalidate = false; export const revalidate = false;
@@ -293,17 +295,32 @@ export async function GET(request: NextRequest) {
* This means that the user should not be prompted to enter their password again. * 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 * 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({ const selectedSession = await findValidSession({
serviceUrl, serviceUrl,
sessions, sessions,
authRequest, authRequest,
}); });
if (!selectedSession || !selectedSession.id) { const noSessionResponse = NextResponse.json(
return NextResponse.json(
{ error: "No active session found" }, { error: "No active session found" },
{ status: 400 }, { 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( const cookie = sessionCookies.find(
@@ -311,10 +328,7 @@ export async function GET(request: NextRequest) {
); );
if (!cookie || !cookie.id || !cookie.token) { if (!cookie || !cookie.id || !cookie.token) {
return NextResponse.json( return noSessionResponse;
{ error: "No active session found" },
{ status: 400 },
);
} }
const session = { 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 { } else {
// check for loginHint, userId hint and valid sessions // check for loginHint, userId hint and valid sessions
let selectedSession = await findValidSession({ let selectedSession = await findValidSession({

View File

@@ -0,0 +1,28 @@
import { createServiceForHost } from "@/lib/service";
import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { Client } from "@zitadel/client";
import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb";
import { headers } from "next/headers";
import { NextResponse } from "next/server";
export async function GET() {
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const settingsService: Client<typeof SettingsService> =
await createServiceForHost(SettingsService, serviceUrl);
const settings = await settingsService
.getSecuritySettings({})
.then((resp) => (resp.settings ? resp.settings : undefined));
const response = NextResponse.json({ settings }, { status: 200 });
// Add Cache-Control header to cache the response for up to 1 hour
response.headers.set(
"Cache-Control",
"public, max-age=3600, stale-while-revalidate=86400",
);
return response;
}

View File

@@ -20,7 +20,10 @@ export type Cookie = {
type SessionCookie<T> = Cookie & T; type SessionCookie<T> = Cookie & T;
async function setSessionHttpOnlyCookie<T>(sessions: SessionCookie<T>[]) { async function setSessionHttpOnlyCookie<T>(
sessions: SessionCookie<T>[],
sameSite: boolean | "lax" | "strict" | "none" = true,
) {
const cookiesList = await cookies(); const cookiesList = await cookies();
return cookiesList.set({ return cookiesList.set({
@@ -28,6 +31,7 @@ async function setSessionHttpOnlyCookie<T>(sessions: SessionCookie<T>[]) {
value: JSON.stringify(sessions), value: JSON.stringify(sessions),
httpOnly: true, httpOnly: true,
path: "/", path: "/",
sameSite,
}); });
} }
@@ -42,10 +46,15 @@ export async function setLanguageCookie(language: string) {
}); });
} }
export async function addSessionToCookie<T>( export async function addSessionToCookie<T>({
session: SessionCookie<T>, session,
cleanup: boolean = false, cleanup,
): Promise<any> { sameSite,
}: {
session: SessionCookie<T>;
cleanup?: boolean;
sameSite?: boolean | "lax" | "strict" | "none" | undefined;
}): Promise<any> {
const cookiesList = await cookies(); const cookiesList = await cookies();
const stringifiedCookie = cookiesList.get("sessions"); const stringifiedCookie = cookiesList.get("sessions");
@@ -79,17 +88,23 @@ export async function addSessionToCookie<T>(
? timestampDate(timestampFromMs(Number(session.expirationTs))) > now ? timestampDate(timestampFromMs(Number(session.expirationTs))) > now
: true, : true,
); );
return setSessionHttpOnlyCookie(filteredSessions); return setSessionHttpOnlyCookie(filteredSessions, sameSite);
} else { } else {
return setSessionHttpOnlyCookie(currentSessions); return setSessionHttpOnlyCookie(currentSessions, sameSite);
} }
} }
export async function updateSessionCookie<T>( export async function updateSessionCookie<T>({
id: string, id,
session: SessionCookie<T>, session,
cleanup: boolean = false, cleanup,
): Promise<any> { sameSite,
}: {
id: string;
session: SessionCookie<T>;
cleanup?: boolean;
sameSite?: boolean | "lax" | "strict" | "none" | undefined;
}): Promise<any> {
const cookiesList = await cookies(); const cookiesList = await cookies();
const stringifiedCookie = cookiesList.get("sessions"); const stringifiedCookie = cookiesList.get("sessions");
@@ -108,19 +123,24 @@ export async function updateSessionCookie<T>(
? timestampDate(timestampFromMs(Number(session.expirationTs))) > now ? timestampDate(timestampFromMs(Number(session.expirationTs))) > now
: true, : true,
); );
return setSessionHttpOnlyCookie(filteredSessions); return setSessionHttpOnlyCookie(filteredSessions, sameSite);
} else { } else {
return setSessionHttpOnlyCookie(sessions); return setSessionHttpOnlyCookie(sessions, sameSite);
} }
} else { } else {
throw "updateSessionCookie<T>: session id now found"; throw "updateSessionCookie<T>: session id now found";
} }
} }
export async function removeSessionFromCookie<T>( export async function removeSessionFromCookie<T>({
session: SessionCookie<T>, session,
cleanup: boolean = false, cleanup,
): Promise<any> { sameSite,
}: {
session: SessionCookie<T>;
cleanup?: boolean;
sameSite?: boolean | "lax" | "strict" | "none" | undefined;
}): Promise<any> {
const cookiesList = await cookies(); const cookiesList = await cookies();
const stringifiedCookie = cookiesList.get("sessions"); const stringifiedCookie = cookiesList.get("sessions");
@@ -136,9 +156,9 @@ export async function removeSessionFromCookie<T>(
? timestampDate(timestampFromMs(Number(session.expirationTs))) > now ? timestampDate(timestampFromMs(Number(session.expirationTs))) > now
: true, : true,
); );
return setSessionHttpOnlyCookie(filteredSessions); return setSessionHttpOnlyCookie(filteredSessions, sameSite);
} else { } else {
return setSessionHttpOnlyCookie(reducedSessions); return setSessionHttpOnlyCookie(reducedSessions, sameSite);
} }
} }

View File

@@ -8,7 +8,7 @@ import {
} from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb"; } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { constructUrl } from "./service"; import { constructUrl } from "./service-url";
import { isSessionValid } from "./session"; import { isSessionValid } from "./session";
type LoginWithOIDCAndSession = { type LoginWithOIDCAndSession = {

View File

@@ -5,7 +5,7 @@ import { create } from "@zitadel/client";
import { CreateResponseRequestSchema } from "@zitadel/proto/zitadel/saml/v2/saml_service_pb"; import { CreateResponseRequestSchema } from "@zitadel/proto/zitadel/saml/v2/saml_service_pb";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { constructUrl } from "./service"; import { constructUrl } from "./service-url";
import { isSessionValid } from "./session"; import { isSessionValid } from "./session";
type LoginWithSAMLAndSession = { type LoginWithSAMLAndSession = {

View File

@@ -4,7 +4,7 @@ import { createServerTransport } from "@zitadel/client/node";
import { createUserServiceClient } from "@zitadel/client/v2"; import { createUserServiceClient } from "@zitadel/client/v2";
import { headers } from "next/headers"; import { headers } from "next/headers";
import { getSessionCookieById } from "./cookies"; import { getSessionCookieById } from "./cookies";
import { getServiceUrlFromHeaders } from "./service"; import { getServiceUrlFromHeaders } from "./service-url";
import { getSession } from "./zitadel"; import { getSession } from "./zitadel";
const transport = async (serviceUrl: string, token: string) => { const transport = async (serviceUrl: string, token: string) => {

View File

@@ -4,6 +4,7 @@ import { addSessionToCookie, updateSessionCookie } from "@/lib/cookies";
import { import {
createSessionForUserIdAndIdpIntent, createSessionForUserIdAndIdpIntent,
createSessionFromChecks, createSessionFromChecks,
getSecuritySettings,
getSession, getSession,
setSession, setSession,
} from "@/lib/zitadel"; } from "@/lib/zitadel";
@@ -20,7 +21,7 @@ import {
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { headers } from "next/headers"; import { headers } from "next/headers";
import { getServiceUrlFromHeaders } from "../service"; import { getServiceUrlFromHeaders } from "../service-url";
type CustomCookieData = { type CustomCookieData = {
id: string; id: string;
@@ -65,7 +66,7 @@ export async function createSessionAndUpdateCookie(command: {
serviceUrl, serviceUrl,
sessionId: createdSession.sessionId, sessionId: createdSession.sessionId,
sessionToken: createdSession.sessionToken, sessionToken: createdSession.sessionToken,
}).then((response) => { }).then(async (response) => {
if (response?.session && response.session?.factors?.user?.loginName) { if (response?.session && response.session?.factors?.user?.loginName) {
const sessionCookie: CustomCookieData = { const sessionCookie: CustomCookieData = {
id: createdSession.sessionId, id: createdSession.sessionId,
@@ -91,9 +92,14 @@ export async function createSessionAndUpdateCookie(command: {
response.session.factors.user.organizationId; response.session.factors.user.organizationId;
} }
return addSessionToCookie(sessionCookie).then(() => { const securitySettings = await getSecuritySettings({ serviceUrl });
const sameSite = securitySettings?.embeddedIframe?.enabled
? "none"
: true;
await addSessionToCookie({ session: sessionCookie, sameSite });
return response.session as Session; return response.session as Session;
});
} else { } else {
throw "could not get session or session does not have loginName"; throw "could not get session or session does not have loginName";
} }
@@ -167,7 +173,10 @@ export async function createSessionForIdpAndUpdateCookie(
sessionCookie.organization = session.factors.user.organizationId; sessionCookie.organization = session.factors.user.organizationId;
} }
return addSessionToCookie(sessionCookie).then(() => { const securitySettings = await getSecuritySettings({ serviceUrl });
const sameSite = securitySettings?.embeddedIframe?.enabled ? "none" : true;
return addSessionToCookie({ session: sessionCookie, sameSite }).then(() => {
return session as Session; return session as Session;
}); });
} }
@@ -217,8 +226,14 @@ export async function setSessionAndUpdateCookie(
serviceUrl, serviceUrl,
sessionId: sessionCookie.id, sessionId: sessionCookie.id,
sessionToken: sessionCookie.token, sessionToken: sessionCookie.token,
}).then((response) => { }).then(async (response) => {
if (response?.session && response.session.factors?.user?.loginName) { if (
!response?.session ||
!response.session.factors?.user?.loginName
) {
throw "could not get session or session does not have loginName";
}
const { session } = response; const { session } = response;
const newCookie: CustomCookieData = { const newCookie: CustomCookieData = {
id: sessionCookie.id, id: sessionCookie.id,
@@ -237,12 +252,18 @@ export async function setSessionAndUpdateCookie(
newCookie.requestId = sessionCookie.requestId; newCookie.requestId = sessionCookie.requestId;
} }
return updateSessionCookie(sessionCookie.id, newCookie).then(() => { const securitySettings = await getSecuritySettings({ serviceUrl });
const sameSite = securitySettings?.embeddedIframe?.enabled
? "none"
: true;
return updateSessionCookie({
id: sessionCookie.id,
session: newCookie,
sameSite,
}).then(() => {
return { challenges: updatedSession.challenges, ...session }; return { challenges: updatedSession.challenges, ...session };
}); });
} else {
throw "could not get session or session does not have loginName";
}
}); });
} else { } else {
throw "Session not be set"; throw "Session not be set";

View File

@@ -2,7 +2,7 @@
import { authorizeOrDenyDeviceAuthorization } from "@/lib/zitadel"; import { authorizeOrDenyDeviceAuthorization } from "@/lib/zitadel";
import { headers } from "next/headers"; import { headers } from "next/headers";
import { getServiceUrlFromHeaders } from "../service"; import { getServiceUrlFromHeaders } from "../service-url";
export async function completeDeviceAuthorization( export async function completeDeviceAuthorization(
deviceAuthorizationId: string, deviceAuthorizationId: string,

View File

@@ -8,7 +8,7 @@ import {
import { headers } from "next/headers"; import { headers } from "next/headers";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getNextUrl } from "../client"; import { getNextUrl } from "../client";
import { getServiceUrlFromHeaders } from "../service"; import { getServiceUrlFromHeaders } from "../service-url";
import { checkEmailVerification } from "../verify-helper"; import { checkEmailVerification } from "../verify-helper";
import { createSessionForIdpAndUpdateCookie } from "./cookie"; import { createSessionForIdpAndUpdateCookie } from "./cookie";

View File

@@ -3,7 +3,7 @@
import { addHumanUser, createInviteCode } from "@/lib/zitadel"; import { addHumanUser, createInviteCode } from "@/lib/zitadel";
import { Factors } from "@zitadel/proto/zitadel/session/v2/session_pb"; import { Factors } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { headers } from "next/headers"; import { headers } from "next/headers";
import { getServiceUrlFromHeaders } from "../service"; import { getServiceUrlFromHeaders } from "../service-url";
type InviteUserCommand = { type InviteUserCommand = {
email: string; email: string;

View File

@@ -8,7 +8,7 @@ import { idpTypeToIdentityProviderType, idpTypeToSlug } from "../idp";
import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { UserState } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { UserState } from "@zitadel/proto/zitadel/user/v2/user_pb";
import { getServiceUrlFromHeaders } from "../service"; import { getServiceUrlFromHeaders } from "../service-url";
import { checkInvite } from "../verify-helper"; import { checkInvite } from "../verify-helper";
import { import {
getActiveIdentityProviders, getActiveIdentityProviders,

View File

@@ -2,7 +2,7 @@
import { getDeviceAuthorizationRequest as zitadelGetDeviceAuthorizationRequest } from "@/lib/zitadel"; import { getDeviceAuthorizationRequest as zitadelGetDeviceAuthorizationRequest } from "@/lib/zitadel";
import { headers } from "next/headers"; import { headers } from "next/headers";
import { getServiceUrlFromHeaders } from "../service"; import { getServiceUrlFromHeaders } from "../service-url";
export async function getDeviceAuthorizationRequest(userCode: string) { export async function getDeviceAuthorizationRequest(userCode: string) {
const _headers = await headers(); const _headers = await headers();

View File

@@ -13,7 +13,7 @@ import {
getSessionCookieById, getSessionCookieById,
getSessionCookieByLoginName, getSessionCookieByLoginName,
} from "../cookies"; } from "../cookies";
import { getServiceUrlFromHeaders } from "../service"; import { getServiceUrlFromHeaders } from "../service-url";
import { getLoginSettings } from "../zitadel"; import { getLoginSettings } from "../zitadel";
export type SetOTPCommand = { export type SetOTPCommand = {

View File

@@ -22,7 +22,7 @@ import {
getSessionCookieById, getSessionCookieById,
getSessionCookieByLoginName, getSessionCookieByLoginName,
} from "../cookies"; } from "../cookies";
import { getServiceUrlFromHeaders } from "../service"; import { getServiceUrlFromHeaders } from "../service-url";
import { checkEmailVerification } from "../verify-helper"; import { checkEmailVerification } from "../verify-helper";
import { setSessionAndUpdateCookie } from "./cookie"; import { setSessionAndUpdateCookie } from "./cookie";

View File

@@ -32,7 +32,7 @@ import {
import { headers } from "next/headers"; import { headers } from "next/headers";
import { getNextUrl } from "../client"; import { getNextUrl } from "../client";
import { getSessionCookieById, getSessionCookieByLoginName } from "../cookies"; import { getSessionCookieById, getSessionCookieByLoginName } from "../cookies";
import { getServiceUrlFromHeaders } from "../service"; import { getServiceUrlFromHeaders } from "../service-url";
import { import {
checkEmailVerification, checkEmailVerification,
checkMFAFactors, checkMFAFactors,

View File

@@ -10,7 +10,7 @@ import {
} from "@zitadel/proto/zitadel/session/v2/session_service_pb"; } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { headers } from "next/headers"; import { headers } from "next/headers";
import { getNextUrl } from "../client"; import { getNextUrl } from "../client";
import { getServiceUrlFromHeaders } from "../service"; import { getServiceUrlFromHeaders } from "../service-url";
import { checkEmailVerification } from "../verify-helper"; import { checkEmailVerification } from "../verify-helper";
type RegisterUserCommand = { type RegisterUserCommand = {

View File

@@ -4,6 +4,7 @@ import { setSessionAndUpdateCookie } from "@/lib/server/cookie";
import { import {
deleteSession, deleteSession,
getLoginSettings, getLoginSettings,
getSecuritySettings,
humanMFAInitSkipped, humanMFAInitSkipped,
listAuthenticationMethodTypes, listAuthenticationMethodTypes,
} from "@/lib/zitadel"; } from "@/lib/zitadel";
@@ -19,7 +20,7 @@ import {
getSessionCookieByLoginName, getSessionCookieByLoginName,
removeSessionFromCookie, removeSessionFromCookie,
} from "../cookies"; } from "../cookies";
import { getServiceUrlFromHeaders } from "../service"; import { getServiceUrlFromHeaders } from "../service-url";
export async function skipMFAAndContinueWithNextUrl({ export async function skipMFAAndContinueWithNextUrl({
userId, userId,
@@ -209,8 +210,11 @@ export async function clearSession(options: ClearSessionOptions) {
sessionToken: session.token, sessionToken: session.token,
}); });
const securitySettings = await getSecuritySettings({ serviceUrl });
const sameSite = securitySettings?.embeddedIframe?.enabled ? "none" : true;
if (deletedSession) { if (deletedSession) {
return removeSessionFromCookie(session); return removeSessionFromCookie({ session, sameSite });
} }
} }
@@ -230,9 +234,12 @@ export async function cleanupSession({ sessionId }: CleanupSessionCommand) {
sessionToken: sessionCookie.token, sessionToken: sessionCookie.token,
}); });
const securitySettings = await getSecuritySettings({ serviceUrl });
const sameSite = securitySettings?.embeddedIframe?.enabled ? "none" : true;
if (!deleteResponse) { if (!deleteResponse) {
throw new Error("Could not delete session"); throw new Error("Could not delete session");
} }
return removeSessionFromCookie(sessionCookie); return removeSessionFromCookie({ session: sessionCookie, sameSite });
} }

View File

@@ -6,7 +6,7 @@ import { VerifyU2FRegistrationRequestSchema } from "@zitadel/proto/zitadel/user/
import { headers } from "next/headers"; import { headers } from "next/headers";
import { userAgent } from "next/server"; import { userAgent } from "next/server";
import { getSessionCookieById } from "../cookies"; import { getSessionCookieById } from "../cookies";
import { getServiceUrlFromHeaders } from "../service"; import { getServiceUrlFromHeaders } from "../service-url";
type RegisterU2FCommand = { type RegisterU2FCommand = {
sessionId: string; sessionId: string;

View File

@@ -19,7 +19,7 @@ import { User } from "@zitadel/proto/zitadel/user/v2/user_pb";
import { headers } from "next/headers"; import { headers } from "next/headers";
import { getNextUrl } from "../client"; import { getNextUrl } from "../client";
import { getSessionCookieByLoginName } from "../cookies"; import { getSessionCookieByLoginName } from "../cookies";
import { getServiceUrlFromHeaders } from "../service"; import { getServiceUrlFromHeaders } from "../service-url";
import { loadMostRecentSession } from "../session"; import { loadMostRecentSession } from "../session";
import { checkMFAFactors } from "../verify-helper"; import { checkMFAFactors } from "../verify-helper";
import { createSessionAndUpdateCookie } from "./cookie"; import { createSessionAndUpdateCookie } from "./cookie";

View File

@@ -0,0 +1,58 @@
import { ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers";
import { NextRequest } from "next/server";
/**
* Extracts the service url and region from the headers if used in a multitenant context (host, x-zitadel-forward-host header)
* or falls back to the ZITADEL_API_URL for a self hosting deployment
* or falls back to the host header for a self hosting deployment using custom domains
* @param headers
* @returns the service url and region from the headers
* @throws if the service url could not be determined
*
*/
export function getServiceUrlFromHeaders(headers: ReadonlyHeaders): {
serviceUrl: string;
} {
let instanceUrl;
const forwardedHost = headers.get("x-zitadel-forward-host");
// use the forwarded host if available (multitenant), otherwise fall back to the host of the deployment itself
if (forwardedHost) {
instanceUrl = forwardedHost;
instanceUrl = instanceUrl.startsWith("http://")
? instanceUrl
: `https://${instanceUrl}`;
} else if (process.env.ZITADEL_API_URL) {
instanceUrl = process.env.ZITADEL_API_URL;
} else {
const host = headers.get("host");
if (host) {
const [hostname, port] = host.split(":");
if (hostname !== "localhost") {
instanceUrl = host.startsWith("http") ? host : `https://${host}`;
}
}
}
if (!instanceUrl) {
throw new Error("Service URL could not be determined");
}
return {
serviceUrl: instanceUrl,
};
}
export function constructUrl(request: NextRequest, path: string) {
const forwardedProto = request.headers.get("x-forwarded-proto")
? `${request.headers.get("x-forwarded-proto")}:`
: request.nextUrl.protocol;
const forwardedHost =
request.headers.get("x-zitadel-forward-host") ??
request.headers.get("x-forwarded-host") ??
request.headers.get("host");
const basePath = process.env.NEXT_PUBLIC_BASE_PATH || "";
return new URL(`${basePath}${path}`, `${forwardedProto}//${forwardedHost}`);
}

View File

@@ -7,8 +7,6 @@ import { SAMLService } from "@zitadel/proto/zitadel/saml/v2/saml_service_pb";
import { SessionService } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; import { SessionService } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb"; import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb";
import { UserService } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import { UserService } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers";
import { NextRequest } from "next/server";
import { systemAPIToken } from "./api"; import { systemAPIToken } from "./api";
type ServiceClass = type ServiceClass =
@@ -66,59 +64,3 @@ export async function createServiceForHost<T extends ServiceClass>(
return createClientFor<T>(service)(transport); return createClientFor<T>(service)(transport);
} }
/**
* Extracts the service url and region from the headers if used in a multitenant context (host, x-zitadel-forward-host header)
* or falls back to the ZITADEL_API_URL for a self hosting deployment
* or falls back to the host header for a self hosting deployment using custom domains
* @param headers
* @returns the service url and region from the headers
* @throws if the service url could not be determined
*
*/
export function getServiceUrlFromHeaders(headers: ReadonlyHeaders): {
serviceUrl: string;
} {
let instanceUrl;
const forwardedHost = headers.get("x-zitadel-forward-host");
// use the forwarded host if available (multitenant), otherwise fall back to the host of the deployment itself
if (forwardedHost) {
instanceUrl = forwardedHost;
instanceUrl = instanceUrl.startsWith("http://")
? instanceUrl
: `https://${instanceUrl}`;
} else if (process.env.ZITADEL_API_URL) {
instanceUrl = process.env.ZITADEL_API_URL;
} else {
const host = headers.get("host");
if (host) {
const [hostname, port] = host.split(":");
if (hostname !== "localhost") {
instanceUrl = host.startsWith("http") ? host : `https://${host}`;
}
}
}
if (!instanceUrl) {
throw new Error("Service URL could not be determined");
}
return {
serviceUrl: instanceUrl,
};
}
export function constructUrl(request: NextRequest, path: string) {
const forwardedProto = request.headers.get("x-forwarded-proto")
? `${request.headers.get("x-forwarded-proto")}:`
: request.nextUrl.protocol;
const forwardedHost =
request.headers.get("x-zitadel-forward-host") ??
request.headers.get("x-forwarded-host") ??
request.headers.get("host");
const basePath = process.env.NEXT_PUBLIC_BASE_PATH || "";
return new URL(`${basePath}${path}`, `${forwardedProto}//${forwardedHost}`);
}

View File

@@ -92,6 +92,21 @@ export async function getLoginSettings({
return useCache ? cacheWrapper(callback) : callback; return useCache ? cacheWrapper(callback) : callback;
} }
export async function getSecuritySettings({
serviceUrl,
}: {
serviceUrl: string;
}) {
const settingsService: Client<typeof SettingsService> =
await createServiceForHost(SettingsService, serviceUrl);
const callback = settingsService
.getSecuritySettings({})
.then((resp) => (resp.settings ? resp.settings : undefined));
return useCache ? cacheWrapper(callback) : callback;
}
export async function getLockoutSettings({ export async function getLockoutSettings({
serviceUrl, serviceUrl,
orgId, orgId,

View File

@@ -1,6 +1,7 @@
import { headers } from "next/headers"; import { headers } from "next/headers";
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { getServiceUrlFromHeaders } from "./lib/service"; import { DEFAULT_CSP } from "../constants/csp";
import { getServiceUrlFromHeaders } from "./lib/service-url";
export const config = { export const config = {
matcher: [ matcher: [
@@ -22,6 +23,20 @@ export async function middleware(request: NextRequest) {
const { serviceUrl } = getServiceUrlFromHeaders(_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) {
console.error(
"Failed to fetch security settings:",
securityResponse.statusText,
);
return NextResponse.next(); // Fallback if the request fails
}
const { settings: securitySettings } = await securityResponse.json();
const instanceHost = `${serviceUrl}` const instanceHost = `${serviceUrl}`
.replace("https://", "") .replace("https://", "")
.replace("http://", ""); .replace("http://", "");
@@ -39,7 +54,17 @@ export async function middleware(request: NextRequest) {
responseHeaders.set("Access-Control-Allow-Origin", "*"); responseHeaders.set("Access-Control-Allow-Origin", "*");
responseHeaders.set("Access-Control-Allow-Headers", "*"); 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(" ")};`,
);
responseHeaders.delete("X-Frame-Options");
}
request.nextUrl.href = `${serviceUrl}${request.nextUrl.pathname}${request.nextUrl.search}`; request.nextUrl.href = `${serviceUrl}${request.nextUrl.pathname}${request.nextUrl.search}`;
return NextResponse.rewrite(request.nextUrl, { return NextResponse.rewrite(request.nextUrl, {
request: { request: {
headers: requestHeaders, headers: requestHeaders,