Merge commit from fork

* fix: Improve host header handling and deployment mode detection

* ZITADEL_CLOUD env

* refactor service config

* service config

* cleanup

* cleanup

* util for original and public host

* instanceHost, publicHost

* cleanup constructUrl

* cleanup middleware

* add x-zitadel-forward-host as fallback for current cloud setup

* fix build

* fix: lint

* fix: update test mocks

* fix: provide basepath to unit tests

* host handling

* unit tests

* imports

* fix: getPublicHost instead of getInstanceHost

* unit tests

* update nextjs, implement jwt verify for logout_token

* pass logout information in jwt to login v2

* handle hosts in jwks url

* point idp callbacks to the api and not the login UI

* logout_hint out of logout token, typecheck

* rm unused imports

* fix oidc integration test

---------

Co-authored-by: Livio Spring <[email protected]>
This commit is contained in:
Max Peintner
2025-12-08 10:01:49 +01:00
committed by GitHub
co-authored by Livio Spring
parent f9e3871bb3
commit df75be96ff
75 changed files with 2075 additions and 1817 deletions
+8 -14
View File
@@ -3,8 +3,8 @@
import { getAllSessions } from "@/lib/cookies";
import { loginWithOIDCAndSession } from "@/lib/oidc";
import { loginWithSAMLAndSession } from "@/lib/saml";
import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { listSessions } from "@/lib/zitadel";
import { getServiceConfig } from "@/lib/service-url";
import { listSessions, ServiceConfig } from "@/lib/zitadel";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { headers } from "next/headers";
@@ -14,10 +14,8 @@ export interface AuthFlowParams {
organization?: string;
}
async function loadSessions({ serviceUrl, ids }: { serviceUrl: string; ids: string[] }): Promise<Session[]> {
const response = await listSessions({
serviceUrl,
ids: ids.filter((id: string | undefined) => !!id),
async function loadSessions({ serviceConfig, ids }: { serviceConfig: ServiceConfig; ids: string[] }): Promise<Session[]> {
const response = await listSessions({ serviceConfig, ids: ids.filter((id: string | undefined) => !!id),
});
return response?.sessions ?? [];
@@ -33,21 +31,19 @@ export async function completeAuthFlow(command: AuthFlowParams): Promise<{ error
const { sessionId, requestId } = command;
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const { serviceConfig } = getServiceConfig(_headers);
const sessionCookies = await getAllSessions();
const ids = sessionCookies.map((s) => s.id);
let sessions: Session[] = [];
if (ids && ids.length) {
sessions = await loadSessions({ serviceUrl, ids });
sessions = await loadSessions({ serviceConfig, ids });
}
if (requestId.startsWith("oidc_")) {
// Complete OIDC flow
const result = await loginWithOIDCAndSession({
serviceUrl,
authRequest: requestId.replace("oidc_", ""),
const result = await loginWithOIDCAndSession({ serviceConfig, authRequest: requestId.replace("oidc_", ""),
sessionId,
sessions,
sessionCookies,
@@ -62,9 +58,7 @@ export async function completeAuthFlow(command: AuthFlowParams): Promise<{ error
return result;
} else if (requestId.startsWith("saml_")) {
// Complete SAML flow
const result = await loginWithSAMLAndSession({
serviceUrl,
samlRequest: requestId.replace("saml_", ""),
const result = await loginWithSAMLAndSession({ serviceConfig, samlRequest: requestId.replace("saml_", ""),
sessionId,
sessions,
sessionCookies,
+13 -25
View File
@@ -14,7 +14,7 @@ import { Challenges, RequestChallenges } from "@zitadel/proto/zitadel/session/v2
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { headers } from "next/headers";
import { getServiceUrlFromHeaders } from "../service-url";
import { getServiceConfig } from "../service-url";
type CustomCookieData = {
id: string;
@@ -46,7 +46,7 @@ export async function createSessionAndUpdateCookie(command: {
lifetime?: Duration;
}): Promise<Session> {
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const { serviceConfig } = getServiceConfig(_headers);
let sessionLifetime = command.lifetime;
@@ -59,16 +59,12 @@ export async function createSessionAndUpdateCookie(command: {
} as Duration; // for usecases where the lifetime is not specified (user discovery)
}
const createdSession = await createSessionFromChecks({
serviceUrl,
checks: command.checks,
const createdSession = await createSessionFromChecks({ serviceConfig, checks: command.checks,
lifetime: sessionLifetime,
});
if (createdSession) {
return getSession({
serviceUrl,
sessionId: createdSession.sessionId,
return getSession({ serviceConfig, sessionId: createdSession.sessionId,
sessionToken: createdSession.sessionToken,
}).then(async (response) => {
if (response?.session && response.session?.factors?.user?.loginName) {
@@ -89,7 +85,7 @@ export async function createSessionAndUpdateCookie(command: {
sessionCookie.organization = response.session.factors.user.organizationId;
}
const securitySettings = await getSecuritySettings({ serviceUrl });
const securitySettings = await getSecuritySettings({ serviceConfig });
const iFrameEnabled = !!securitySettings?.embeddedIframe?.enabled;
await addSessionToCookie({ session: sessionCookie, iFrameEnabled });
@@ -119,7 +115,7 @@ export async function createSessionForIdpAndUpdateCookie({
lifetime?: Duration;
}): Promise<Session> {
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const { serviceConfig } = getServiceConfig(_headers);
let sessionLifetime = lifetime;
@@ -132,9 +128,7 @@ export async function createSessionForIdpAndUpdateCookie({
} as Duration;
}
const createdSession = await createSessionForUserIdAndIdpIntent({
serviceUrl,
userId,
const createdSession = await createSessionForUserIdAndIdpIntent({ serviceConfig, userId,
idpIntent,
lifetime: sessionLifetime,
}).catch((error: ErrorDetail | CredentialsCheckError) => {
@@ -152,9 +146,7 @@ export async function createSessionForIdpAndUpdateCookie({
throw "Could not create session";
}
const { session } = await getSession({
serviceUrl,
sessionId: createdSession.sessionId,
const { session } = await getSession({ serviceConfig, sessionId: createdSession.sessionId,
sessionToken: createdSession.sessionToken,
});
@@ -180,7 +172,7 @@ export async function createSessionForIdpAndUpdateCookie({
sessionCookie.organization = session.factors.user.organizationId;
}
const securitySettings = await getSecuritySettings({ serviceUrl });
const securitySettings = await getSecuritySettings({ serviceConfig });
const iFrameEnabled = !!securitySettings?.embeddedIframe?.enabled;
return addSessionToCookie({ session: sessionCookie, iFrameEnabled }).then(() => {
@@ -200,11 +192,9 @@ export async function setSessionAndUpdateCookie(command: {
lifetime: Duration;
}) {
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const { serviceConfig } = getServiceConfig(_headers);
return setSession({
serviceUrl,
sessionId: command.recentCookie.id,
return setSession({ serviceConfig, sessionId: command.recentCookie.id,
sessionToken: command.recentCookie.token,
challenges: command.challenges,
checks: command.checks,
@@ -227,9 +217,7 @@ export async function setSessionAndUpdateCookie(command: {
sessionCookie.requestId = command.requestId;
}
return getSession({
serviceUrl,
sessionId: sessionCookie.id,
return getSession({ serviceConfig, sessionId: sessionCookie.id,
sessionToken: sessionCookie.token,
}).then(async (response) => {
if (!response?.session || !response.session.factors?.user?.loginName) {
@@ -252,7 +240,7 @@ export async function setSessionAndUpdateCookie(command: {
newCookie.requestId = sessionCookie.requestId;
}
const securitySettings = await getSecuritySettings({ serviceUrl });
const securitySettings = await getSecuritySettings({ serviceConfig });
const iFrameEnabled = !!securitySettings?.embeddedIframe?.enabled;
return updateSessionCookie({
+3 -5
View File
@@ -2,19 +2,17 @@
import { authorizeOrDenyDeviceAuthorization } from "@/lib/zitadel";
import { headers } from "next/headers";
import { getServiceUrlFromHeaders } from "../service-url";
import { getServiceConfig } from "../service-url";
export async function completeDeviceAuthorization(
deviceAuthorizationId: string,
session?: { sessionId: string; sessionToken: string },
) {
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const { serviceConfig } = getServiceConfig(_headers);
// without the session, device auth request is denied
return authorizeOrDenyDeviceAuthorization({
serviceUrl,
deviceAuthorizationId,
return authorizeOrDenyDeviceAuthorization({ serviceConfig, deviceAuthorizationId,
session,
});
}
+16 -39
View File
@@ -9,6 +9,7 @@ import {
getSAMLRequest,
getSecuritySettings,
startIdentityProviderFlow,
ServiceConfig,
} from "@/lib/zitadel";
import { sendLoginname, SendLoginnameCommand } from "@/lib/server/loginname";
import { idpTypeToSlug } from "@/lib/idp";
@@ -47,7 +48,7 @@ const gotoAccounts = ({
};
export interface FlowInitiationParams {
serviceUrl: string;
serviceConfig: ServiceConfig;
requestId: string;
sessions: Session[];
sessionCookies: any[];
@@ -58,11 +59,9 @@ export interface FlowInitiationParams {
* Handle OIDC flow initiation
*/
export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Promise<NextResponse> {
const { serviceUrl, requestId, sessions, sessionCookies, request } = params;
const { serviceConfig, requestId, sessions, sessionCookies, request } = params;
const { authRequest } = await getAuthRequest({
serviceUrl,
authRequestId: requestId.replace("oidc_", ""),
const { authRequest } = await getAuthRequest({ serviceConfig, authRequestId: requestId.replace("oidc_", ""),
});
let organization = "";
@@ -85,9 +84,7 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
console.log("Extracted org domain:", orgDomain);
if (orgDomain) {
const orgs = await getOrgsByDomain({
serviceUrl,
domain: orgDomain,
const orgs = await getOrgsByDomain({ serviceConfig, domain: orgDomain,
});
if (orgs.result && orgs.result.length === 1) {
@@ -102,9 +99,7 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
const matched = IDP_SCOPE_REGEX.exec(idpScope);
idpId = matched?.[1] ?? "";
const identityProviders = await getActiveIdentityProviders({
serviceUrl,
orgId: organization ? organization : undefined,
const identityProviders = await getActiveIdentityProviders({ serviceConfig, orgId: organization ? organization : undefined,
}).then((resp) => {
return resp.identityProviders;
});
@@ -137,9 +132,7 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
params.set("organization", organization);
}
let url: string | null = await startIdentityProviderFlow({
serviceUrl,
idpId,
let url: string | null = await startIdentityProviderFlow({ serviceConfig, idpId,
urls: {
successUrl: `${origin}/idp/${provider}/process?` + new URLSearchParams(params),
failureUrl: `${origin}/idp/${provider}/failure?` + new URLSearchParams(params),
@@ -215,13 +208,9 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
}
return NextResponse.redirect(loginNameUrl);
} else if (authRequest.prompt.includes(Prompt.NONE)) {
const securitySettings = await getSecuritySettings({
serviceUrl,
});
const securitySettings = await getSecuritySettings({ serviceConfig, });
const selectedSession = await findValidSession({
serviceUrl,
sessions,
const selectedSession = await findValidSession({ serviceConfig, sessions,
authRequest,
});
@@ -251,9 +240,7 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
sessionToken: cookie.token,
};
const { callbackUrl } = await createCallback({
serviceUrl,
req: create(CreateCallbackRequestSchema, {
const { callbackUrl } = await createCallback({ serviceConfig, req: create(CreateCallbackRequestSchema, {
authRequestId: requestId.replace("oidc_", ""),
callbackKind: {
case: "session",
@@ -275,9 +262,7 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
return callbackResponse;
} else {
let selectedSession = await findValidSession({
serviceUrl,
sessions,
let selectedSession = await findValidSession({ serviceConfig, sessions,
authRequest,
});
@@ -305,9 +290,7 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
};
try {
const { callbackUrl } = await createCallback({
serviceUrl,
req: create(CreateCallbackRequestSchema, {
const { callbackUrl } = await createCallback({ serviceConfig, req: create(CreateCallbackRequestSchema, {
authRequestId: requestId.replace("oidc_", ""),
callbackKind: {
case: "session",
@@ -359,11 +342,9 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
* Handle SAML flow initiation
*/
export async function handleSAMLFlowInitiation(params: FlowInitiationParams): Promise<NextResponse> {
const { serviceUrl, requestId, sessions, sessionCookies, request } = params;
const { serviceConfig, requestId, sessions, sessionCookies, request } = params;
const { samlRequest } = await getSAMLRequest({
serviceUrl,
samlRequestId: requestId.replace("saml_", ""),
const { samlRequest } = await getSAMLRequest({ serviceConfig, samlRequestId: requestId.replace("saml_", ""),
});
if (!samlRequest) {
@@ -378,9 +359,7 @@ export async function handleSAMLFlowInitiation(params: FlowInitiationParams): Pr
}
// Try to find a valid session
let selectedSession = await findValidSession({
serviceUrl,
sessions,
let selectedSession = await findValidSession({ serviceConfig, sessions,
samlRequest,
});
@@ -410,9 +389,7 @@ export async function handleSAMLFlowInitiation(params: FlowInitiationParams): Pr
};
try {
const { url, binding } = await createResponse({
serviceUrl,
req: create(CreateResponseRequestSchema, {
const { url, binding } = await createResponse({ serviceConfig, req: create(CreateResponseRequestSchema, {
samlRequestId: requestId.replace("saml_", ""),
responseKind: {
case: "session",
+162 -161
View File
@@ -1,10 +1,5 @@
import { describe, expect, test, vi, beforeEach, afterEach } from "vitest";
import { getOriginalHost, getOriginalHostWithProtocol } from "./host";
// Mock the Next.js headers function
vi.mock("next/headers", () => ({
headers: vi.fn(),
}));
import { getInstanceHost, getPublicHostWithProtocol, getPublicHost } from "./host";
describe("Host utility functions", () => {
beforeEach(() => {
@@ -15,151 +10,94 @@ describe("Host utility functions", () => {
vi.restoreAllMocks();
});
describe("getOriginalHost", () => {
test("should return x-forwarded-host when available", async () => {
const { headers } = await import("next/headers");
describe("getInstanceHost", () => {
test("should use x-zitadel-instance-host when available", () => {
const mockHeaders = {
get: vi.fn((key: string) => {
if (key === "x-forwarded-host") return "zitadel.com";
if (key === "x-original-host") return "backup.com";
if (key === "host") return "internal.vercel.app";
if (key === "x-zitadel-instance-host") return "instance.zitadel.cloud";
if (key === "x-zitadel-forward-host") return "forward.zitadel.cloud";
return null;
}),
};
} as any;
vi.mocked(headers).mockResolvedValue(mockHeaders as any);
const result = await getOriginalHost();
expect(result).toBe("zitadel.com");
expect(mockHeaders.get).toHaveBeenCalledWith("x-forwarded-host");
const result = getInstanceHost(mockHeaders);
expect(result).toBe("instance.zitadel.cloud");
expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-instance-host");
});
test("should fall back to x-original-host when x-forwarded-host is not available", async () => {
const { headers } = await import("next/headers");
test("should use x-zitadel-forward-host when x-zitadel-instance-host is not available", () => {
const mockHeaders = {
get: vi.fn((key: string) => {
if (key === "x-forwarded-host") return null;
if (key === "x-original-host") return "original.com";
if (key === "host") return "internal.vercel.app";
if (key === "x-zitadel-instance-host") return null;
if (key === "x-zitadel-forward-host") return "forward.zitadel.cloud";
return null;
}),
};
} as any;
vi.mocked(headers).mockResolvedValue(mockHeaders as any);
const result = await getOriginalHost();
expect(result).toBe("original.com");
expect(mockHeaders.get).toHaveBeenCalledWith("x-forwarded-host");
expect(mockHeaders.get).toHaveBeenCalledWith("x-original-host");
const result = getInstanceHost(mockHeaders);
expect(result).toBe("forward.zitadel.cloud");
expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-instance-host");
expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-forward-host");
});
test("should fall back to host when forwarded headers are not available", async () => {
const { headers } = await import("next/headers");
test("should return null when neither x-zitadel-instance-host nor x-zitadel-forward-host are available", () => {
const mockHeaders = {
get: vi.fn((key: string) => {
if (key === "x-forwarded-host") return null;
if (key === "x-original-host") return null;
if (key === "host") return "fallback.com";
if (key === "x-zitadel-instance-host") return null;
if (key === "x-zitadel-forward-host") return null;
if (key === "x-forwarded-host") return "accounts.mycompany.com";
if (key === "host") return "internal.server";
return null;
}),
};
} as any;
vi.mocked(headers).mockResolvedValue(mockHeaders as any);
const result = await getOriginalHost();
expect(result).toBe("fallback.com");
expect(mockHeaders.get).toHaveBeenCalledWith("x-forwarded-host");
expect(mockHeaders.get).toHaveBeenCalledWith("x-original-host");
expect(mockHeaders.get).toHaveBeenCalledWith("host");
});
test("should throw error when no host is found", async () => {
const { headers } = await import("next/headers");
const mockHeaders = {
get: vi.fn(() => null),
};
vi.mocked(headers).mockResolvedValue(mockHeaders as any);
await expect(getOriginalHost()).rejects.toThrow("No host found in headers");
});
test("should throw error when host is empty string", async () => {
const { headers } = await import("next/headers");
const mockHeaders = {
get: vi.fn(() => ""),
};
vi.mocked(headers).mockResolvedValue(mockHeaders as any);
await expect(getOriginalHost()).rejects.toThrow("No host found in headers");
});
test("should throw error when host is not a string", async () => {
const { headers } = await import("next/headers");
const mockHeaders = {
get: vi.fn(() => 123),
};
vi.mocked(headers).mockResolvedValue(mockHeaders as any);
await expect(getOriginalHost()).rejects.toThrow("No host found in headers");
const result = getInstanceHost(mockHeaders);
expect(result).toBeNull();
expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-instance-host");
expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-forward-host");
});
});
describe("getOriginalHostWithProtocol", () => {
test("should return https for production domain", async () => {
const { headers } = await import("next/headers");
describe("getPublicHostWithProtocol", () => {
test("should return https for production domain", () => {
const mockHeaders = {
get: vi.fn(() => "zitadel.com"),
};
} as any;
vi.mocked(headers).mockResolvedValue(mockHeaders as any);
const result = await getOriginalHostWithProtocol();
const result = getPublicHostWithProtocol(mockHeaders);
expect(result).toBe("https://zitadel.com");
});
test("should return http for localhost", async () => {
const { headers } = await import("next/headers");
test("should return http for localhost", () => {
const mockHeaders = {
get: vi.fn(() => "localhost:3000"),
};
} as any;
vi.mocked(headers).mockResolvedValue(mockHeaders as any);
const result = await getOriginalHostWithProtocol();
const result = getPublicHostWithProtocol(mockHeaders);
expect(result).toBe("http://localhost:3000");
});
test("should return http for localhost without port", async () => {
const { headers } = await import("next/headers");
test("should return http for localhost without port", () => {
const mockHeaders = {
get: vi.fn(() => "localhost"),
};
} as any;
vi.mocked(headers).mockResolvedValue(mockHeaders as any);
const result = await getOriginalHostWithProtocol();
const result = getPublicHostWithProtocol(mockHeaders);
expect(result).toBe("http://localhost");
});
test("should return https for custom domain", async () => {
const { headers } = await import("next/headers");
test("should return https for custom domain", () => {
const mockHeaders = {
get: vi.fn(() => "auth.company.com"),
};
} as any;
vi.mocked(headers).mockResolvedValue(mockHeaders as any);
const result = await getOriginalHostWithProtocol();
const result = getPublicHostWithProtocol(mockHeaders);
expect(result).toBe("https://auth.company.com");
});
});
describe("Real-world scenarios", () => {
test("should handle Vercel rewrite scenario", async () => {
const { headers } = await import("next/headers");
test("should handle Vercel rewrite scenario", () => {
const mockHeaders = {
get: vi.fn((key: string) => {
// Simulate Vercel rewrite: zitadel.com/login -> login-zitadel-qa.vercel.app
@@ -167,16 +105,13 @@ describe("Host utility functions", () => {
if (key === "host") return "login-zitadel-qa.vercel.app";
return null;
}),
};
} as any;
vi.mocked(headers).mockResolvedValue(mockHeaders as any);
const result = await getOriginalHostWithProtocol();
const result = getPublicHostWithProtocol(mockHeaders);
expect(result).toBe("https://zitadel.com");
});
test("should handle CloudFlare proxy scenario", async () => {
const { headers } = await import("next/headers");
test("should handle CloudFlare proxy scenario", () => {
const mockHeaders = {
get: vi.fn((key: string) => {
if (key === "x-forwarded-host") return "auth.company.com";
@@ -184,114 +119,180 @@ describe("Host utility functions", () => {
if (key === "host") return "cloudflare-worker.workers.dev";
return null;
}),
};
} as any;
vi.mocked(headers).mockResolvedValue(mockHeaders as any);
const result = await getOriginalHost();
const result = getPublicHost(mockHeaders);
expect(result).toBe("auth.company.com");
});
test("should handle development environment", async () => {
const { headers } = await import("next/headers");
test("should handle development environment", () => {
const mockHeaders = {
get: vi.fn((key: string) => {
if (key === "host") return "localhost:3000";
return null;
}),
};
} as any;
vi.mocked(headers).mockResolvedValue(mockHeaders as any);
const result = await getOriginalHostWithProtocol();
const result = getPublicHostWithProtocol(mockHeaders);
expect(result).toBe("http://localhost:3000");
});
test("should handle staging environment with subdomain", async () => {
const { headers } = await import("next/headers");
test("should handle staging environment with subdomain", () => {
const mockHeaders = {
get: vi.fn((key: string) => {
if (key === "x-forwarded-host") return "staging-auth.company.com";
if (key === "host") return "staging-internal.vercel.app";
return null;
}),
};
} as any;
vi.mocked(headers).mockResolvedValue(mockHeaders as any);
const result = await getOriginalHostWithProtocol();
const result = getPublicHostWithProtocol(mockHeaders);
expect(result).toBe("https://staging-auth.company.com");
});
test("should prioritize x-zitadel-instance-host in multi-tenant scenario", () => {
const mockHeaders = {
get: vi.fn((key: string) => {
if (key === "x-zitadel-instance-host") return "customer.zitadel.cloud";
if (key === "x-forwarded-host") return "accounts.company.com";
if (key === "host") return "internal.vercel.app";
return null;
}),
} as any;
const result = getInstanceHost(mockHeaders);
expect(result).toBe("customer.zitadel.cloud");
});
});
describe("Edge cases", () => {
test("should handle IPv4 addresses", async () => {
const { headers } = await import("next/headers");
test("should handle IPv4 addresses", () => {
const mockHeaders = {
get: vi.fn(() => "192.168.1.100:3000"),
};
} as any;
vi.mocked(headers).mockResolvedValue(mockHeaders as any);
const result = await getOriginalHostWithProtocol();
const result = getPublicHostWithProtocol(mockHeaders);
expect(result).toBe("https://192.168.1.100:3000");
});
test("should handle IPv6 addresses", async () => {
const { headers } = await import("next/headers");
test("should handle IPv6 addresses", () => {
const mockHeaders = {
get: vi.fn(() => "[::1]:3000"),
};
} as any;
vi.mocked(headers).mockResolvedValue(mockHeaders as any);
const result = await getOriginalHostWithProtocol();
const result = getPublicHostWithProtocol(mockHeaders);
expect(result).toBe("https://[::1]:3000");
});
test("should handle hosts with ports", async () => {
const { headers } = await import("next/headers");
test("should handle hosts with ports", () => {
const mockHeaders = {
get: vi.fn(() => "zitadel.com:8080"),
};
} as any;
vi.mocked(headers).mockResolvedValue(mockHeaders as any);
const result = await getOriginalHostWithProtocol();
const result = getPublicHostWithProtocol(mockHeaders);
expect(result).toBe("https://zitadel.com:8080");
});
test("should handle localhost with different ports", async () => {
const { headers } = await import("next/headers");
test("should handle localhost with different ports", () => {
const mockHeaders = {
get: vi.fn(() => "localhost:8080"),
};
} as any;
vi.mocked(headers).mockResolvedValue(mockHeaders as any);
const result = await getOriginalHostWithProtocol();
const result = getPublicHostWithProtocol(mockHeaders);
expect(result).toBe("http://localhost:8080");
});
});
test("should handle priority order correctly", async () => {
const { headers } = await import("next/headers");
describe("getPublicHost", () => {
test("should use x-zitadel-public-host when available", () => {
const mockHeaders = {
get: vi.fn((key: string) => {
// All headers are present, should return x-forwarded-host (highest priority)
if (key === "x-forwarded-host") return "priority1.com";
if (key === "x-original-host") return "priority2.com";
if (key === "host") return "priority3.com";
if (key === "x-zitadel-public-host") return "public.zitadel.cloud";
if (key === "x-forwarded-host") return "accounts.company.com";
return null;
}),
};
} as any;
vi.mocked(headers).mockResolvedValue(mockHeaders as any);
const result = getPublicHost(mockHeaders);
expect(result).toBe("public.zitadel.cloud");
expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-public-host");
});
const result = await getOriginalHost();
expect(result).toBe("priority1.com");
// Should only call x-forwarded-host since it's available
test("should use x-zitadel-forward-host when x-zitadel-public-host is not available", () => {
const mockHeaders = {
get: vi.fn((key: string) => {
if (key === "x-zitadel-public-host") return null;
if (key === "x-zitadel-forward-host") return "forward.zitadel.cloud";
if (key === "x-forwarded-host") return "accounts.company.com";
return null;
}),
} as any;
const result = getPublicHost(mockHeaders);
expect(result).toBe("forward.zitadel.cloud");
expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-public-host");
expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-forward-host");
});
test("should use x-forwarded-host when neither x-zitadel-public-host nor x-zitadel-forward-host is available", () => {
const mockHeaders = {
get: vi.fn((key: string) => {
if (key === "x-zitadel-public-host") return null;
if (key === "x-zitadel-forward-host") return null;
if (key === "x-forwarded-host") return "accounts.company.com";
if (key === "host") return "internal.server";
return null;
}),
} as any;
const result = getPublicHost(mockHeaders);
expect(result).toBe("accounts.company.com");
expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-public-host");
expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-forward-host");
expect(mockHeaders.get).toHaveBeenCalledWith("x-forwarded-host");
expect(mockHeaders.get).toHaveBeenCalledTimes(1);
});
test("should fall back to host when x-forwarded-host is not available", () => {
const mockHeaders = {
get: vi.fn((key: string) => {
if (key === "x-zitadel-public-host") return null;
if (key === "x-zitadel-forward-host") return null;
if (key === "x-forwarded-host") return null;
if (key === "host") return "localhost:3000";
return null;
}),
} as any;
const result = getPublicHost(mockHeaders);
expect(result).toBe("localhost:3000");
expect(mockHeaders.get).toHaveBeenCalledWith("x-forwarded-host");
expect(mockHeaders.get).toHaveBeenCalledWith("host");
});
test("should throw error when no host is found", () => {
const mockHeaders = {
get: vi.fn(() => null),
} as any;
expect(() => getPublicHost(mockHeaders)).toThrow("No host found in headers");
});
test("should differ from getInstanceHost when x-zitadel-instance-host is present", () => {
const mockHeaders = {
get: vi.fn((key: string) => {
if (key === "x-zitadel-instance-host") return "instance.zitadel.cloud";
if (key === "x-forwarded-host") return "accounts.company.com";
if (key === "host") return "internal.server";
return null;
}),
} as any;
const instanceHost = getInstanceHost(mockHeaders);
const publicHost = getPublicHost(mockHeaders);
expect(instanceHost).toBe("instance.zitadel.cloud");
expect(publicHost).toBe("accounts.company.com");
expect(instanceHost).not.toBe(publicHost);
});
});
});
+30 -30
View File
@@ -1,48 +1,48 @@
import { headers } from "next/headers";
import { ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers";
/**
* Gets the original host that the user sees in their browser URL.
* When using rewrites this function prioritizes forwarded headers that preserve the original host.
*
* ⚠️ SERVER-SIDE ONLY: This function can only be used in:
* - Server Actions (functions with "use server")
* - Server Components (React components that run on the server)
* - Route Handlers (API routes)
* - Middleware
*
* @returns The host string (e.g., "zitadel.com")
* @throws Error if no host is found
*/
export async function getOriginalHost(): Promise<string> {
const _headers = await headers();
export function getInstanceHost(headers: ReadonlyHeaders): string | null {
// use standard proxy headers (x-forwarded-host → host) for both multi-tenant and self-hosted, do not use x-zitadel-instance-host
const instanceHost = headers.get("x-zitadel-instance-host") || headers.get("x-zitadel-forward-host");
// Priority order:
// 1. x-forwarded-host - Set by proxies/CDNs with the original host
// 2. x-original-host - Alternative header sometimes used
// 3. host - Fallback to the current host header
const host = _headers.get("x-forwarded-host") || _headers.get("x-original-host") || _headers.get("host");
if (!host || typeof host !== "string") {
throw new Error("No host found in headers");
}
return host;
return instanceHost;
}
/**
* Gets the original host with protocol prefix.
* Automatically detects if localhost should use http:// or https://
* Gets the public host that the user sees in their browser URL.
* Only considers standard proxy headers (x-forwarded-host and host).
* Does NOT include x-zitadel-instance-host.
*
* ⚠️ SERVER-SIDE ONLY: This function can only be used in:
* - Server Actions (functions with "use server")
* - Server Components (React components that run on the server)
* - Route Handlers (API routes)
* - Middleware
* Use this when you need the public-facing host that the user actually sees,
* not the internal instance host used for API routing.
*
* @returns The full URL prefix (e.g., "https://zitadel.com")
* @returns The public host string (e.g., "accounts.company.com")
* @throws Error if no host is found
*/
export async function getOriginalHostWithProtocol(): Promise<string> {
const host = await getOriginalHost();
export function getPublicHost(headers: ReadonlyHeaders): string {
// Only use standard proxy headers (x-zitadel-public-host → x-zitadel-forward-host → x-forwarded-host → host)
// Do NOT use x-zitadel-instance-host as it may differ from what the user sees
const publicHost =
headers.get("x-zitadel-public-host") ||
headers.get("x-zitadel-forward-host") ||
headers.get("x-forwarded-host") ||
headers.get("host");
if (!publicHost || typeof publicHost !== "string") {
throw new Error("No host found in headers");
}
return publicHost;
}
export function getPublicHostWithProtocol(headers: ReadonlyHeaders): string {
const host = getPublicHost(headers);
const protocol = host.includes("localhost") ? "http://" : "https://";
return `${protocol}${host}`;
}
+30 -30
View File
@@ -12,7 +12,7 @@ vi.mock("@zitadel/client", () => ({
}));
vi.mock("../service-url", () => ({
getServiceUrlFromHeaders: vi.fn(),
getServiceConfig: vi.fn(),
}));
vi.mock("../zitadel", () => ({
@@ -111,7 +111,7 @@ describe("processIDPCallback", () => {
// Import mocked modules
const { headers } = await import("next/headers");
const { getServiceUrlFromHeaders } = await import("../service-url");
const { getServiceConfig } = await import("../service-url");
const {
retrieveIDPIntent,
getIDPByID,
@@ -129,7 +129,7 @@ describe("processIDPCallback", () => {
// Setup mocks
mockHeaders = vi.mocked(headers);
mockGetServiceUrlFromHeaders = vi.mocked(getServiceUrlFromHeaders);
mockGetServiceUrlFromHeaders = vi.mocked(getServiceConfig);
mockRetrieveIDPIntent = vi.mocked(retrieveIDPIntent);
mockGetIDPByID = vi.mocked(getIDPByID);
mockUpdateHuman = vi.mocked(updateHuman);
@@ -146,7 +146,7 @@ describe("processIDPCallback", () => {
// Default mock implementations
mockHeaders.mockResolvedValue({} as any);
mockGetServiceUrlFromHeaders.mockReturnValue({
serviceUrl: "https://api.example.com",
serviceConfig: { baseUrl: "https://api.example.com" },
});
mockRetrieveIDPIntent.mockResolvedValue(defaultIntent);
mockGetIDPByID.mockResolvedValue(defaultIdp);
@@ -257,7 +257,7 @@ describe("processIDPCallback", () => {
const result = await processIDPCallback(defaultParams);
expect(mockRetrieveIDPIntent).toHaveBeenCalledWith({
serviceUrl: "https://api.example.com",
serviceConfig: { baseUrl: "https://api.example.com" },
id: "intent123",
token: "token123",
});
@@ -287,7 +287,7 @@ describe("processIDPCallback", () => {
await processIDPCallback(defaultParams);
expect(mockUpdateHuman).toHaveBeenCalledWith({
serviceUrl: "https://api.example.com",
serviceConfig: { baseUrl: "https://api.example.com" },
request: expect.objectContaining({
userId: "user123",
profile: defaultIntent.updateHumanUser.profile,
@@ -353,7 +353,7 @@ describe("processIDPCallback", () => {
const result = await processIDPCallback(linkParams);
expect(mockAddIDPLink).toHaveBeenCalledWith({
serviceUrl: "https://api.example.com",
serviceConfig: { baseUrl: "https://api.example.com" },
idp: {
id: "idp123",
userId: "user123",
@@ -442,12 +442,12 @@ describe("processIDPCallback", () => {
const result = await processIDPCallback(defaultParams);
expect(mockListUsers).toHaveBeenCalledWith({
serviceUrl: "https://api.example.com",
serviceConfig: { baseUrl: "https://api.example.com" },
email: "[email protected]",
organizationId: "org123",
});
expect(mockAddIDPLink).toHaveBeenCalledWith({
serviceUrl: "https://api.example.com",
serviceConfig: { baseUrl: "https://api.example.com" },
idp: {
id: "idp123",
userId: "user123",
@@ -523,7 +523,7 @@ describe("processIDPCallback", () => {
const result = await processIDPCallback(defaultParams);
expect(mockListUsers).toHaveBeenCalledWith({
serviceUrl: "https://api.example.com",
serviceConfig: { baseUrl: "https://api.example.com" },
userName: "testuser",
organizationId: "org123",
});
@@ -557,7 +557,7 @@ describe("processIDPCallback", () => {
const result = await processIDPCallback(defaultParams);
expect(mockAddHuman).toHaveBeenCalledWith({
serviceUrl: "https://api.example.com",
serviceConfig: { baseUrl: "https://api.example.com" },
request: expect.objectContaining({
username: "testuser",
profile: defaultIntent.addHumanUser.profile,
@@ -602,11 +602,11 @@ describe("processIDPCallback", () => {
});
expect(mockGetOrgsByDomain).toHaveBeenCalledWith({
serviceUrl: "https://api.example.com",
serviceConfig: { baseUrl: "https://api.example.com" },
domain: "example.com",
});
expect(mockAddHuman).toHaveBeenCalledWith({
serviceUrl: "https://api.example.com",
serviceConfig: { baseUrl: "https://api.example.com" },
request: expect.objectContaining({
organization: expect.objectContaining({
org: { case: "orgId", value: "org-from-domain" },
@@ -625,10 +625,10 @@ describe("processIDPCallback", () => {
});
expect(mockGetDefaultOrg).toHaveBeenCalledWith({
serviceUrl: "https://api.example.com",
serviceConfig: { baseUrl: "https://api.example.com" },
});
expect(mockAddHuman).toHaveBeenCalledWith({
serviceUrl: "https://api.example.com",
serviceConfig: { baseUrl: "https://api.example.com" },
request: expect.objectContaining({
organization: expect.objectContaining({
org: { case: "orgId", value: "default-org" },
@@ -901,14 +901,14 @@ describe("validateIDPLinkingPermissions", () => {
});
const result = await validateIDPLinkingPermissions({
serviceUrl: "https://api.example.com",
serviceConfig: { baseUrl: "https://api.example.com" },
userOrganizationId: "org123",
idpId: "idp123",
});
expect(result).toBe(false);
expect(mockGetLoginSettings).toHaveBeenCalledWith({
serviceUrl: "https://api.example.com",
serviceConfig: { baseUrl: "https://api.example.com" },
organization: "org123",
});
expect(mockGetActiveIdentityProviders).not.toHaveBeenCalled();
@@ -918,7 +918,7 @@ describe("validateIDPLinkingPermissions", () => {
mockGetLoginSettings.mockResolvedValue(undefined);
const result = await validateIDPLinkingPermissions({
serviceUrl: "https://api.example.com",
serviceConfig: { baseUrl: "https://api.example.com" },
userOrganizationId: "org123",
idpId: "idp123",
});
@@ -930,7 +930,7 @@ describe("validateIDPLinkingPermissions", () => {
mockGetLoginSettings.mockResolvedValue({});
const result = await validateIDPLinkingPermissions({
serviceUrl: "https://api.example.com",
serviceConfig: { baseUrl: "https://api.example.com" },
userOrganizationId: "org123",
idpId: "idp123",
});
@@ -952,14 +952,14 @@ describe("validateIDPLinkingPermissions", () => {
});
const result = await validateIDPLinkingPermissions({
serviceUrl: "https://api.example.com",
serviceConfig: { baseUrl: "https://api.example.com" },
userOrganizationId: "org123",
idpId: "idp123",
});
expect(result).toBe(false);
expect(mockGetActiveIdentityProviders).toHaveBeenCalledWith({
serviceUrl: "https://api.example.com",
serviceConfig: { baseUrl: "https://api.example.com" },
orgId: "org123",
linking_allowed: true,
});
@@ -974,7 +974,7 @@ describe("validateIDPLinkingPermissions", () => {
});
const result = await validateIDPLinkingPermissions({
serviceUrl: "https://api.example.com",
serviceConfig: { baseUrl: "https://api.example.com" },
userOrganizationId: "org123",
idpId: "idp123",
});
@@ -989,7 +989,7 @@ describe("validateIDPLinkingPermissions", () => {
mockGetActiveIdentityProviders.mockResolvedValue({});
const result = await validateIDPLinkingPermissions({
serviceUrl: "https://api.example.com",
serviceConfig: { baseUrl: "https://api.example.com" },
userOrganizationId: "org123",
idpId: "idp123",
});
@@ -1011,18 +1011,18 @@ describe("validateIDPLinkingPermissions", () => {
});
const result = await validateIDPLinkingPermissions({
serviceUrl: "https://api.example.com",
serviceConfig: { baseUrl: "https://api.example.com" },
userOrganizationId: "org123",
idpId: "idp123",
});
expect(result).toBe(true);
expect(mockGetLoginSettings).toHaveBeenCalledWith({
serviceUrl: "https://api.example.com",
serviceConfig: { baseUrl: "https://api.example.com" },
organization: "org123",
});
expect(mockGetActiveIdentityProviders).toHaveBeenCalledWith({
serviceUrl: "https://api.example.com",
serviceConfig: { baseUrl: "https://api.example.com" },
orgId: "org123",
linking_allowed: true,
});
@@ -1042,7 +1042,7 @@ describe("validateIDPLinkingPermissions", () => {
});
const result = await validateIDPLinkingPermissions({
serviceUrl: "https://api.example.com",
serviceConfig: { baseUrl: "https://api.example.com" },
userOrganizationId: "org123",
idpId: "idp123",
});
@@ -1057,7 +1057,7 @@ describe("validateIDPLinkingPermissions", () => {
await expect(
validateIDPLinkingPermissions({
serviceUrl: "https://api.example.com",
serviceConfig: { baseUrl: "https://api.example.com" },
userOrganizationId: "org123",
idpId: "idp123",
}),
@@ -1072,7 +1072,7 @@ describe("validateIDPLinkingPermissions", () => {
await expect(
validateIDPLinkingPermissions({
serviceUrl: "https://api.example.com",
serviceConfig: { baseUrl: "https://api.example.com" },
userOrganizationId: "org123",
idpId: "idp123",
}),
@@ -1088,7 +1088,7 @@ describe("validateIDPLinkingPermissions", () => {
});
const result = await validateIDPLinkingPermissions({
serviceUrl: "https://api.example.com",
serviceConfig: { baseUrl: "https://api.example.com" },
userOrganizationId: "org123",
idpId: "idp123",
});
+26 -55
View File
@@ -1,6 +1,6 @@
"use server";
import { getServiceUrlFromHeaders } from "@/lib/service-url";
import { getServiceConfig } from "@/lib/service-url";
import {
retrieveIDPIntent,
getIDPByID,
@@ -13,6 +13,7 @@ import {
getActiveIdentityProviders,
getUserByID,
getDefaultOrg,
ServiceConfig,
} from "@/lib/zitadel";
import { headers } from "next/headers";
import { create } from "@zitadel/client";
@@ -31,11 +32,11 @@ const ORG_SUFFIX_REGEX = /(?<=@)(.+)/;
async function resolveOrganizationForUser({
organization,
addHumanUser,
serviceUrl,
serviceConfig,
}: {
organization?: string;
addHumanUser?: AddHumanUserRequest;
serviceUrl: string;
serviceConfig: ServiceConfig;
}): Promise<string | undefined> {
if (organization) return organization;
@@ -43,16 +44,12 @@ async function resolveOrganizationForUser({
const matched = ORG_SUFFIX_REGEX.exec(addHumanUser.username);
const suffix = matched?.[1] ?? "";
const orgs = await getOrgsByDomain({
serviceUrl,
domain: suffix,
const orgs = await getOrgsByDomain({ serviceConfig, domain: suffix,
});
const orgToCheckForDiscovery = orgs.result && orgs.result.length === 1 ? orgs.result[0].id : undefined;
if (orgToCheckForDiscovery) {
const orgLoginSettings = await getLoginSettings({
serviceUrl,
organization: orgToCheckForDiscovery,
const orgLoginSettings = await getLoginSettings({ serviceConfig, organization: orgToCheckForDiscovery,
});
if (orgLoginSettings?.allowDomainDiscovery) {
return orgToCheckForDiscovery;
@@ -61,7 +58,7 @@ async function resolveOrganizationForUser({
}
// Fallback to default organization if no org was resolved through discovery
const defaultOrg = await getDefaultOrg({ serviceUrl });
const defaultOrg = await getDefaultOrg({ serviceConfig });
return defaultOrg?.id;
}
@@ -72,19 +69,15 @@ async function resolveOrganizationForUser({
* 2. The specific IDP is activated for the organization
*
*/
export async function validateIDPLinkingPermissions({
serviceUrl,
userOrganizationId,
export async function validateIDPLinkingPermissions({ serviceConfig, userOrganizationId,
idpId,
}: {
serviceUrl: string;
serviceConfig: ServiceConfig;
userOrganizationId: string;
idpId: string;
}): Promise<boolean> {
// Check organization login settings
const loginSettings = await getLoginSettings({
serviceUrl,
organization: userOrganizationId,
const loginSettings = await getLoginSettings({ serviceConfig, organization: userOrganizationId,
});
if (!loginSettings?.allowExternalIdp) {
@@ -92,9 +85,7 @@ export async function validateIDPLinkingPermissions({
}
// Check if the IDP is activated for the organization and allows linking
const activeIDPs = await getActiveIdentityProviders({
serviceUrl,
orgId: userOrganizationId,
const activeIDPs = await getActiveIdentityProviders({ serviceConfig, orgId: userOrganizationId,
linking_allowed: true,
});
@@ -132,7 +123,7 @@ export async function processIDPCallback({
postErrorRedirectUrl?: string;
}): Promise<{ redirect?: string; error?: string }> {
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const { serviceConfig } = getServiceConfig(_headers);
const t = await getTranslations("idp");
@@ -155,9 +146,7 @@ export async function processIDPCallback({
});
// Consume the single-use token ONCE
const intent = await retrieveIDPIntent({
serviceUrl,
id,
const intent = await retrieveIDPIntent({ serviceConfig, id,
token,
});
@@ -171,9 +160,7 @@ export async function processIDPCallback({
}
// Get IDP configuration
const idp = await getIDPByID({
serviceUrl,
id: idpInformation.idpId,
const idp = await getIDPByID({ serviceConfig, id: idpInformation.idpId,
});
if (!idp) {
@@ -207,9 +194,7 @@ export async function processIDPCallback({
// Auto-update user if enabled
if (options?.isAutoUpdate && updateHumanUser) {
try {
await updateHuman({
serviceUrl,
request: create(UpdateHumanUserRequestSchema, {
await updateHuman({ serviceConfig, request: create(UpdateHumanUserRequestSchema, {
userId: userId,
profile: updateHumanUser.profile,
email: updateHumanUser.email,
@@ -259,7 +244,7 @@ export async function processIDPCallback({
try {
// Get user to retrieve their organization
const targetUser = await getUserByID({ serviceUrl, userId });
const targetUser = await getUserByID({ serviceConfig, userId });
if (!targetUser || !targetUser.details?.resourceOwner) {
console.error("[IDP Process] User not found or missing organization");
@@ -268,9 +253,7 @@ export async function processIDPCallback({
}
// Validate IDP linking permissions
const isAllowed = await validateIDPLinkingPermissions({
serviceUrl,
userOrganizationId: targetUser.details.resourceOwner,
const isAllowed = await validateIDPLinkingPermissions({ serviceConfig, userOrganizationId: targetUser.details.resourceOwner,
idpId: idpInformation.idpId,
});
@@ -280,9 +263,7 @@ export async function processIDPCallback({
return { redirect: `/idp/${provider}/linking-failed?${params}&error=validation_failed` };
}
await addIDPLink({
serviceUrl,
idp: {
await addIDPLink({ serviceConfig, idp: {
id: idpInformation.idpId,
userId: idpInformation.userId,
userName: idpInformation.userName,
@@ -328,21 +309,17 @@ export async function processIDPCallback({
const email = addHumanUser?.email?.email;
if (options.autoLinking === AutoLinkingOption.EMAIL && email) {
foundUser = await listUsers({ serviceUrl, email, organizationId: organization }).then((response) => {
foundUser = await listUsers({ serviceConfig, email, organizationId: organization }).then((response) => {
return response.result ? response.result[0] : null;
});
} else if (options.autoLinking === AutoLinkingOption.USERNAME) {
foundUser = await listUsers({
serviceUrl,
userName: idpInformation.userName,
foundUser = await listUsers({ serviceConfig, userName: idpInformation.userName,
organizationId: organization,
}).then((response) => {
return response.result ? response.result[0] : null;
});
} else {
foundUser = await listUsers({
serviceUrl,
userName: idpInformation.userName,
foundUser = await listUsers({ serviceConfig, userName: idpInformation.userName,
email,
organizationId: organization,
}).then((response) => {
@@ -359,9 +336,7 @@ export async function processIDPCallback({
}
// Validate IDP linking permissions
const isAllowed = await validateIDPLinkingPermissions({
serviceUrl,
userOrganizationId: foundUser.details.resourceOwner,
const isAllowed = await validateIDPLinkingPermissions({ serviceConfig, userOrganizationId: foundUser.details.resourceOwner,
idpId: idpInformation.idpId,
});
@@ -371,9 +346,7 @@ export async function processIDPCallback({
return { redirect: `/idp/${provider}/linking-failed?${params}&error=validation_failed` };
}
await addIDPLink({
serviceUrl,
idp: {
await addIDPLink({ serviceConfig, idp: {
id: idpInformation.idpId,
userId: idpInformation.userId,
userName: idpInformation.userName,
@@ -419,7 +392,7 @@ export async function processIDPCallback({
const orgToRegisterOn = await resolveOrganizationForUser({
organization,
addHumanUser,
serviceUrl,
serviceConfig,
});
if (!orgToRegisterOn) {
@@ -438,9 +411,7 @@ export async function processIDPCallback({
});
try {
const newUser = await addHuman({
serviceUrl,
request: addHumanUserWithOrganization,
const newUser = await addHuman({ serviceConfig, request: addHumanUserWithOrganization,
});
console.log("[IDP Process] User auto-created successfully, creating session");
@@ -480,7 +451,7 @@ export async function processIDPCallback({
const orgToRegisterOn = await resolveOrganizationForUser({
organization,
addHumanUser,
serviceUrl,
serviceConfig,
});
if (!orgToRegisterOn) {
+15 -11
View File
@@ -13,11 +13,12 @@ vi.mock("next/navigation", () => ({
}));
vi.mock("../service-url", () => ({
getServiceUrlFromHeaders: vi.fn(),
getServiceConfig: vi.fn(),
}));
vi.mock("./host", () => ({
getOriginalHost: vi.fn(),
getInstanceHost: vi.fn(),
getPublicHost: vi.fn(),
}));
vi.mock("../zitadel", () => ({
@@ -27,7 +28,8 @@ vi.mock("../zitadel", () => ({
describe("redirectToIdp", () => {
let mockHeaders: any;
let mockGetServiceUrlFromHeaders: any;
let mockGetOriginalHost: any;
let mockGetInstanceHost: any;
let mockGetPublicHost: any;
let mockStartIdentityProviderFlow: any;
beforeEach(async () => {
@@ -35,22 +37,24 @@ describe("redirectToIdp", () => {
// Import mocked modules
const { headers } = await import("next/headers");
const { getServiceUrlFromHeaders } = await import("../service-url");
const { getOriginalHost } = await import("./host");
const { getServiceConfig } = await import("../service-url");
const { getInstanceHost, getPublicHost } = await import("./host");
const { startIdentityProviderFlow } = await import("../zitadel");
// Setup mocks
mockHeaders = vi.mocked(headers);
mockGetServiceUrlFromHeaders = vi.mocked(getServiceUrlFromHeaders);
mockGetOriginalHost = vi.mocked(getOriginalHost);
mockGetServiceUrlFromHeaders = vi.mocked(getServiceConfig);
mockGetInstanceHost = vi.mocked(getInstanceHost);
mockGetPublicHost = vi.mocked(getPublicHost);
mockStartIdentityProviderFlow = vi.mocked(startIdentityProviderFlow);
// Default mock implementations
mockHeaders.mockResolvedValue({} as any);
mockGetServiceUrlFromHeaders.mockReturnValue({
serviceUrl: "https://api.example.com",
serviceConfig: { baseUrl: "https://api.example.com" },
});
mockGetOriginalHost.mockResolvedValue("example.com");
mockGetInstanceHost.mockReturnValue("example.com");
mockGetPublicHost.mockReturnValue("example.com");
});
afterEach(() => {
@@ -76,7 +80,7 @@ describe("redirectToIdp", () => {
}
expect(mockStartIdentityProviderFlow).toHaveBeenCalledWith({
serviceUrl: "https://api.example.com",
serviceConfig: { baseUrl: "https://api.example.com" },
idpId: "idp123",
urls: {
successUrl: expect.stringContaining("postErrorRedirectUrl=https%3A%2F%2Fapp.example.com%2Ferror"),
@@ -115,7 +119,7 @@ describe("redirectToIdp", () => {
}
expect(mockStartIdentityProviderFlow).toHaveBeenCalledWith({
serviceUrl: "https://api.example.com",
serviceConfig: { baseUrl: "https://api.example.com" },
idpId: "idp123",
urls: {
successUrl: expect.not.stringContaining("postErrorRedirectUrl"),
+15 -23
View File
@@ -6,21 +6,22 @@ import {
listAuthenticationMethodTypes,
startIdentityProviderFlow,
startLDAPIdentityProviderFlow,
ServiceConfig,
} from "@/lib/zitadel";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
import { completeFlowOrGetUrl } from "../client";
import { getServiceUrlFromHeaders } from "../service-url";
import { getServiceConfig } from "../service-url";
import { checkEmailVerification, checkMFAFactors } from "../verify-helper";
import { createSessionForIdpAndUpdateCookie } from "./cookie";
import { getOriginalHost } from "./host";
import { getPublicHost } from "./host";
export type RedirectToIdpState = { error?: string | null } | undefined;
export async function redirectToIdp(prevState: RedirectToIdpState, formData: FormData): Promise<RedirectToIdpState> {
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const host = await getOriginalHost();
const { serviceConfig } = getServiceConfig(_headers);
const host = getPublicHost(_headers);
const params = new URLSearchParams();
@@ -43,7 +44,7 @@ export async function redirectToIdp(prevState: RedirectToIdpState, formData: For
}
const response = await startIDPFlow({
serviceUrl,
serviceConfig,
host,
idpId,
successUrl: `/idp/${provider}/process?` + params.toString(),
@@ -62,7 +63,7 @@ export async function redirectToIdp(prevState: RedirectToIdpState, formData: For
}
export type StartIDPFlowCommand = {
serviceUrl: string;
serviceConfig: ServiceConfig;
host: string;
idpId: string;
successUrl: string;
@@ -73,7 +74,7 @@ async function startIDPFlow(command: StartIDPFlowCommand) {
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
const url = await startIdentityProviderFlow({
serviceUrl: command.serviceUrl,
serviceConfig: command.serviceConfig,
idpId: command.idpId,
urls: {
successUrl: `${command.host.includes("localhost") ? "http://" : "https://"}${command.host}${basePath}${command.successUrl}`,
@@ -103,25 +104,19 @@ export type CreateNewSessionCommand = {
export async function createNewSessionFromIdpIntent(command: CreateNewSessionCommand) {
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const { serviceConfig } = getServiceConfig(_headers);
if (!command.userId || !command.idpIntent) {
throw new Error("No userId or loginName provided");
}
const userResponse = await getUserByID({
serviceUrl,
userId: command.userId,
});
const userResponse = await getUserByID({ serviceConfig, userId: command.userId });
if (!userResponse || !userResponse.user) {
return { error: "User not found in the system" };
}
const loginSettings = await getLoginSettings({
serviceUrl,
organization: userResponse.user.details?.resourceOwner,
});
const loginSettings = await getLoginSettings({ serviceConfig, organization: userResponse.user.details?.resourceOwner });
const session = await createSessionForIdpAndUpdateCookie({
userId: command.userId,
@@ -146,17 +141,14 @@ export async function createNewSessionFromIdpIntent(command: CreateNewSessionCom
// check if user has MFA methods
let authMethods;
if (session.factors?.user?.id) {
const response = await listAuthenticationMethodTypes({
serviceUrl,
userId: session.factors.user.id,
});
const response = await listAuthenticationMethodTypes({ serviceConfig, userId: session.factors.user.id });
if (response.authMethodTypes && response.authMethodTypes.length) {
authMethods = response.authMethodTypes;
}
}
const mfaFactorCheck = await checkMFAFactors(
serviceUrl,
serviceConfig,
session,
loginSettings,
authMethods || [], // Pass empty array if no auth methods
@@ -193,14 +185,14 @@ type createNewSessionForLDAPCommand = {
export async function createNewSessionForLDAP(command: createNewSessionForLDAPCommand) {
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const { serviceConfig } = getServiceConfig(_headers);
if (!command.username || !command.password) {
return { error: "No username or password provided" };
}
const response = await startLDAPIdentityProviderFlow({
serviceUrl,
serviceConfig,
idpId: command.idpId,
username: command.username,
password: command.password,
+18 -14
View File
@@ -15,7 +15,7 @@ vi.mock("@zitadel/client", () => ({
}));
vi.mock("../service-url", () => ({
getServiceUrlFromHeaders: vi.fn(),
getServiceConfig: vi.fn(),
}));
vi.mock("../idp", () => ({
@@ -39,7 +39,8 @@ vi.mock("./cookie", () => ({
}));
vi.mock("./host", () => ({
getOriginalHost: vi.fn(),
getInstanceHost: vi.fn(),
getPublicHost: vi.fn(),
}));
// this returns the key itself that can be checked not the translated value
@@ -57,7 +58,8 @@ describe("sendLoginname", () => {
let mockCreateSessionAndUpdateCookie: any;
let mockListAuthenticationMethodTypes: any;
let mockListIDPLinks: any;
let mockGetOriginalHost: any;
let mockGetInstanceHost: any;
let mockGetPublicHost: any;
let mockStartIdentityProviderFlow: any;
let mockGetActiveIdentityProviders: any;
let mockGetIDPByID: any;
@@ -70,7 +72,7 @@ describe("sendLoginname", () => {
// Import mocked modules
const { headers } = await import("next/headers");
const { create } = await import("@zitadel/client");
const { getServiceUrlFromHeaders } = await import("../service-url");
const { getServiceConfig } = await import("../service-url");
const {
getLoginSettings,
searchUsers,
@@ -81,19 +83,20 @@ describe("sendLoginname", () => {
getOrgsByDomain,
} = await import("../zitadel");
const { createSessionAndUpdateCookie } = await import("./cookie");
const { getOriginalHost } = await import("./host");
const { getInstanceHost, getPublicHost } = await import("./host");
const { idpTypeToSlug } = await import("../idp");
// Setup mocks
mockHeaders = vi.mocked(headers);
mockCreate = vi.mocked(create);
mockGetServiceUrlFromHeaders = vi.mocked(getServiceUrlFromHeaders);
mockGetServiceUrlFromHeaders = vi.mocked(getServiceConfig);
mockGetLoginSettings = vi.mocked(getLoginSettings);
mockSearchUsers = vi.mocked(searchUsers);
mockCreateSessionAndUpdateCookie = vi.mocked(createSessionAndUpdateCookie);
mockListAuthenticationMethodTypes = vi.mocked(listAuthenticationMethodTypes);
mockListIDPLinks = vi.mocked(listIDPLinks);
mockGetOriginalHost = vi.mocked(getOriginalHost);
mockGetInstanceHost = vi.mocked(getInstanceHost);
mockGetPublicHost = vi.mocked(getPublicHost);
mockStartIdentityProviderFlow = vi.mocked(startIdentityProviderFlow);
mockGetActiveIdentityProviders = vi.mocked(getActiveIdentityProviders);
mockGetIDPByID = vi.mocked(getIDPByID);
@@ -102,8 +105,9 @@ describe("sendLoginname", () => {
// Default mock implementations
mockHeaders.mockResolvedValue({} as any);
mockGetServiceUrlFromHeaders.mockReturnValue({ serviceUrl: "https://api.example.com" });
mockGetOriginalHost.mockResolvedValue("example.com");
mockGetServiceUrlFromHeaders.mockReturnValue({ serviceConfig: { baseUrl: "https://api.example.com" } });
mockGetInstanceHost.mockReturnValue("example.com");
mockGetPublicHost.mockReturnValue("example.com");
mockIdpTypeToSlug.mockReturnValue("google");
mockGetIDPByID.mockResolvedValue({
id: "idp123",
@@ -243,7 +247,7 @@ describe("sendLoginname", () => {
expect(result).toEqual({ redirect: "https://idp.example.com/auth" });
expect(mockListIDPLinks).toHaveBeenCalledWith({
serviceUrl: "https://api.example.com",
serviceConfig: { baseUrl: "https://api.example.com" },
userId: "user123",
});
});
@@ -283,7 +287,7 @@ describe("sendLoginname", () => {
expect(result).toEqual({ redirect: "https://org-idp.example.com/auth" });
expect(mockGetActiveIdentityProviders).toHaveBeenCalledWith({
serviceUrl: "https://api.example.com",
serviceConfig: { baseUrl: "https://api.example.com" },
orgId: "org123", // User's organization from resourceOwner
});
});
@@ -523,7 +527,7 @@ describe("sendLoginname", () => {
// Verify org discovery was called with correct domain
expect(mockGetOrgsByDomain).toHaveBeenCalledWith({
serviceUrl: "https://api.example.com",
serviceConfig: { baseUrl: "https://api.example.com" },
domain: "example.com",
});
});
@@ -562,13 +566,13 @@ describe("sendLoginname", () => {
// Verify org discovery was called
expect(mockGetOrgsByDomain).toHaveBeenCalledWith({
serviceUrl: "https://api.example.com",
serviceConfig: { baseUrl: "https://api.example.com" },
domain: "company.com",
});
// Verify IDP redirect was called with discovered org
expect(mockGetActiveIdentityProviders).toHaveBeenCalledWith({
serviceUrl: "https://api.example.com",
serviceConfig: { baseUrl: "https://api.example.com" },
orgId: "discovered-org-456",
});
});
+18 -42
View File
@@ -9,7 +9,7 @@ import { idpTypeToIdentityProviderType, idpTypeToSlug } from "../idp";
import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { UserState } from "@zitadel/proto/zitadel/user/v2/user_pb";
import { getServiceUrlFromHeaders } from "../service-url";
import { getServiceConfig } from "../service-url";
import {
getActiveIdentityProviders,
getIDPByID,
@@ -22,7 +22,7 @@ import {
startIdentityProviderFlow,
} from "../zitadel";
import { createSessionAndUpdateCookie } from "./cookie";
import { getOriginalHost } from "./host";
import { getPublicHost } from "./host";
import { IDPLink } from "@zitadel/proto/zitadel/user/v2/idp_pb";
export type SendLoginnameCommand = {
@@ -36,21 +36,18 @@ const ORG_SUFFIX_REGEX = /(?<=@)(.+)/;
export async function sendLoginname(command: SendLoginnameCommand) {
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const { serviceConfig } = getServiceConfig(_headers);
const t = await getTranslations("loginname");
const loginSettingsByContext = await getLoginSettings({
serviceUrl,
organization: command.organization,
});
const loginSettingsByContext = await getLoginSettings({ serviceConfig, organization: command.organization });
if (!loginSettingsByContext) {
return { error: t("errors.couldNotGetLoginSettings") };
}
let searchUsersRequest: SearchUsersCommand = {
serviceUrl,
serviceConfig,
searchValue: command.loginName,
organizationId: command.organization,
loginSettings: loginSettingsByContext,
@@ -88,28 +85,22 @@ export async function sendLoginname(command: SendLoginnameCommand) {
// If userId is provided, check for user-specific IDP links first
let identityProviders: IDPLink[] = [];
if (userId) {
identityProviders = await listIDPLinks({
serviceUrl,
userId,
}).then((resp) => {
identityProviders = await listIDPLinks({ serviceConfig, userId }).then((resp) => {
return resp.result;
});
}
// If no IDP links exist for the user (or no userId provided), try to get active IDPs from the organization
if (identityProviders.length === 0) {
const activeIdps = await getActiveIdentityProviders({
serviceUrl,
orgId: organization,
}).then((resp) => {
const activeIdps = await getActiveIdentityProviders({ serviceConfig, orgId: organization }).then((resp) => {
return resp.identityProviders;
});
// If exactly one active IDP exists in the organization, redirect to it
if (activeIdps.length === 1) {
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const host = await getOriginalHost();
const { serviceConfig } = getServiceConfig(_headers);
const host = getPublicHost(_headers);
const identityProviderType = activeIdps[0].type;
const provider = idpTypeToSlug(identityProviderType);
@@ -131,7 +122,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
const url = await startIdentityProviderFlow({
serviceUrl,
serviceConfig,
idpId: activeIdps[0].id,
urls: {
successUrl:
@@ -153,15 +144,12 @@ export async function sendLoginname(command: SendLoginnameCommand) {
if (identityProviders.length === 1) {
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const host = await getOriginalHost();
const { serviceConfig } = getServiceConfig(_headers);
const host = getPublicHost(_headers);
const identityProviderId = identityProviders[0].idpId;
const idp = await getIDPByID({
serviceUrl,
id: identityProviderId,
});
const idp = await getIDPByID({ serviceConfig, id: identityProviderId });
const idpType = idp?.type;
@@ -189,7 +177,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
const url = await startIdentityProviderFlow({
serviceUrl,
serviceConfig,
idpId: idp.id,
urls: {
successUrl:
@@ -216,10 +204,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
const user = users[0];
const userId = users[0].userId;
const userLoginSettings = await getLoginSettings({
serviceUrl,
organization: user.details?.resourceOwner,
});
const userLoginSettings = await getLoginSettings({ serviceConfig, organization: user.details?.resourceOwner });
// compare with the concatenated suffix when set
const concatLoginname = command.suffix ? `${command.loginName}@${command.suffix}` : command.loginName;
@@ -273,10 +258,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
// Resolve organization from command or session
const organization = command.organization ?? session.factors?.user?.organizationId;
const methods = await listAuthenticationMethodTypes({
serviceUrl,
userId: session.factors?.user?.id,
});
const methods = await listAuthenticationMethodTypes({ serviceConfig, userId: session.factors?.user?.id });
// always resend invite if user has no auth method set
if (!methods.authMethodTypes || !methods.authMethodTypes.length) {
@@ -418,18 +400,12 @@ export async function sendLoginname(command: SendLoginnameCommand) {
const suffix = matched?.[1] ?? "";
// this just returns orgs where the suffix is set as primary domain
const orgs = await getOrgsByDomain({
serviceUrl,
domain: suffix,
});
const orgs = await getOrgsByDomain({ serviceConfig, domain: suffix });
const orgToCheckForDiscovery = orgs.result && orgs.result.length === 1 ? orgs.result[0].id : undefined;
if (orgToCheckForDiscovery) {
const orgLoginSettings = await getLoginSettings({
serviceUrl,
organization: orgToCheckForDiscovery,
});
const orgLoginSettings = await getLoginSettings({ serviceConfig, organization: orgToCheckForDiscovery });
if (orgLoginSettings?.allowDomainDiscovery) {
console.log("org discovery successful, using org:", orgToCheckForDiscovery);
+3 -5
View File
@@ -2,14 +2,12 @@
import { getDeviceAuthorizationRequest as zitadelGetDeviceAuthorizationRequest } from "@/lib/zitadel";
import { headers } from "next/headers";
import { getServiceUrlFromHeaders } from "../service-url";
import { getServiceConfig } from "../service-url";
export async function getDeviceAuthorizationRequest(userCode: string) {
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const { serviceConfig } = getServiceConfig(_headers);
return zitadelGetDeviceAuthorizationRequest({
serviceUrl,
userCode,
return zitadelGetDeviceAuthorizationRequest({ serviceConfig, userCode,
});
}
+3 -3
View File
@@ -14,7 +14,7 @@ vi.mock("@zitadel/client", () => ({
}));
vi.mock("../service-url", () => ({
getServiceUrlFromHeaders: vi.fn(),
getServiceConfig: vi.fn(),
}));
vi.mock("../zitadel", () => ({
@@ -62,7 +62,7 @@ describe("sendPasskey", () => {
// Import mocked modules
const { headers } = await import("next/headers");
const { getServiceUrlFromHeaders } = await import("../service-url");
const { getServiceConfig } = await import("../service-url");
const { getLoginSettings, getUserByID } = await import("../zitadel");
const { setSessionAndUpdateCookie } = await import("./cookie");
const { getSessionCookieById, getSessionCookieByLoginName, getMostRecentSessionCookie } = await import("../cookies");
@@ -71,7 +71,7 @@ describe("sendPasskey", () => {
// Setup mocks
mockHeaders = vi.mocked(headers);
mockGetServiceUrlFromHeaders = vi.mocked(getServiceUrlFromHeaders);
mockGetServiceUrlFromHeaders = vi.mocked(getServiceConfig);
mockGetLoginSettings = vi.mocked(getLoginSettings);
mockGetUserByID = vi.mocked(getUserByID);
mockSetSessionAndUpdateCookie = vi.mocked(setSessionAndUpdateCookie);
+16 -47
View File
@@ -20,10 +20,10 @@ import { headers } from "next/headers";
import { userAgent } from "next/server";
import { getTranslations } from "next-intl/server";
import { getMostRecentSessionCookie, getSessionCookieById, getSessionCookieByLoginName } from "../cookies";
import { getServiceUrlFromHeaders } from "../service-url";
import { getServiceConfig } from "../service-url";
import { checkEmailVerification, checkUserVerification } from "../verify-helper";
import { createSessionAndUpdateCookie, setSessionAndUpdateCookie } from "./cookie";
import { getOriginalHost } from "./host";
import { getPublicHost } from "./host";
import { completeFlowOrGetUrl } from "../client";
type VerifyPasskeyCommand = {
@@ -63,8 +63,8 @@ export async function registerPasskeyLink(
}
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const host = await getOriginalHost();
const { serviceConfig } = getServiceConfig(_headers);
const host = getPublicHost(_headers);
let session: GetSessionResponse | undefined;
let createdSession: Session | undefined;
@@ -74,11 +74,7 @@ export async function registerPasskeyLink(
if (command.sessionId) {
// Session-based flow (existing logic)
const sessionCookie = await getSessionCookieById({ sessionId: command.sessionId });
session = await getSession({
serviceUrl,
sessionId: sessionCookie.id,
sessionToken: sessionCookie.token,
});
session = await getSession({ serviceConfig, sessionId: sessionCookie.id, sessionToken: sessionCookie.token });
if (!session?.session?.factors?.user?.id) {
return { error: "Could not determine user from session" };
@@ -89,10 +85,7 @@ export async function registerPasskeyLink(
const sessionValid = isSessionValid(session.session);
if (!sessionValid.valid) {
const authmethods = await listAuthenticationMethodTypes({
serviceUrl,
userId: currentUserId,
});
const authmethods = await listAuthenticationMethodTypes({ serviceConfig, userId: currentUserId });
// if the user has no authmethods set, we need to check if the user was verified
if (authmethods.authMethodTypes.length !== 0) {
@@ -117,10 +110,7 @@ export async function registerPasskeyLink(
code: command.code,
};
} else {
const codeResponse = await createPasskeyRegistrationLink({
serviceUrl,
userId: currentUserId,
});
const codeResponse = await createPasskeyRegistrationLink({ serviceConfig, userId: currentUserId });
if (!codeResponse?.code?.code) {
return { error: "Could not create registration link" };
@@ -136,10 +126,7 @@ export async function registerPasskeyLink(
};
// Check if user exists
const userResponse = await getUserByID({
serviceUrl,
userId: currentUserId,
});
const userResponse = await getUserByID({ serviceConfig, userId: currentUserId });
if (!userResponse || !userResponse.user) {
return { error: "User not found" };
@@ -179,17 +166,12 @@ export async function registerPasskeyLink(
throw new Error("Could not determine user");
}
return registerPasskey({
serviceUrl,
userId: currentUserId,
code: registerCode,
domain: hostname,
});
return registerPasskey({ serviceConfig, userId: currentUserId, code: registerCode, domain: hostname });
}
export async function verifyPasskeyRegistration(command: VerifyPasskeyCommand) {
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const { serviceConfig } = getServiceConfig(_headers);
if (!command.sessionId && !command.userId) {
throw new Error("Either sessionId or userId must be provided");
@@ -214,11 +196,7 @@ export async function verifyPasskeyRegistration(command: VerifyPasskeyCommand) {
const sessionCookie = await getSessionCookieById({
sessionId: command.sessionId,
});
const session = await getSession({
serviceUrl,
sessionId: sessionCookie.id,
sessionToken: sessionCookie.token,
});
const session = await getSession({ serviceConfig, sessionId: sessionCookie.id, sessionToken: sessionCookie.token });
const userId = session?.session?.factors?.user?.id;
if (!userId) {
@@ -231,10 +209,7 @@ export async function verifyPasskeyRegistration(command: VerifyPasskeyCommand) {
currentUserId = command.userId!;
// Verify user exists
const userResponse = await getUserByID({
serviceUrl,
userId: currentUserId,
});
const userResponse = await getUserByID({ serviceConfig, userId: currentUserId });
if (!userResponse || !userResponse.user) {
throw new Error("User not found");
@@ -242,7 +217,7 @@ export async function verifyPasskeyRegistration(command: VerifyPasskeyCommand) {
}
return zitadelVerifyPasskeyRegistration({
serviceUrl,
serviceConfig,
request: create(VerifyPasskeyRegistrationRequestSchema, {
passkeyId: command.passkeyId,
publicKeyCredential: command.publicKeyCredential,
@@ -279,12 +254,9 @@ export async function sendPasskey(command: SendPasskeyCommand) {
}
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const { serviceConfig } = getServiceConfig(_headers);
const loginSettings = await getLoginSettings({
serviceUrl,
organization,
});
const loginSettings = await getLoginSettings({ serviceConfig, organization });
let lifetime = command.lifetime; // Use provided lifetime first
@@ -318,10 +290,7 @@ export async function sendPasskey(command: SendPasskeyCommand) {
let userResponse;
try {
userResponse = await getUserByID({
serviceUrl,
userId: session?.factors?.user?.id,
});
userResponse = await getUserByID({ serviceConfig, userId: session?.factors?.user?.id });
} catch (error) {
console.error("Error fetching user by ID:", error);
return { error: t("verify.errors.couldNotGetUser") };
+32 -74
View File
@@ -10,6 +10,7 @@ import {
listAuthenticationMethodTypes,
listUsers,
passwordReset,
ServiceConfig,
setPassword,
setUserPassword,
} from "@/lib/zitadel";
@@ -22,8 +23,8 @@ import { SetPasswordRequestSchema } from "@zitadel/proto/zitadel/user/v2/user_se
import { headers } from "next/headers";
import { completeFlowOrGetUrl } from "../client";
import { getSessionCookieById, getSessionCookieByLoginName } from "../cookies";
import { getServiceUrlFromHeaders } from "../service-url";
import { getOriginalHostWithProtocol } from "./host";
import { getServiceConfig } from "../service-url";
import { getPublicHostWithProtocol } from "./host";
import {
checkEmailVerification,
checkMFAFactors,
@@ -41,18 +42,14 @@ type ResetPasswordCommand = {
export async function resetPassword(command: ResetPasswordCommand) {
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const { serviceConfig } = getServiceConfig(_headers);
const t = await getTranslations("password");
// Get the original host that the user sees with protocol
const hostWithProtocol = await getOriginalHostWithProtocol();
const hostWithProtocol = await getPublicHostWithProtocol(_headers);
const users = await listUsers({
serviceUrl,
loginName: command.loginName,
organizationId: command.organization,
});
const users = await listUsers({ serviceConfig, loginName: command.loginName, organizationId: command.organization });
if (!users.details || users.details.totalResult !== BigInt(1) || !users.result[0].userId) {
return { error: t("errors.couldNotSendResetLink") };
@@ -62,7 +59,7 @@ export async function resetPassword(command: ResetPasswordCommand) {
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
return passwordReset({
serviceUrl,
serviceConfig,
userId,
urlTemplate:
`${hostWithProtocol}${basePath}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` +
@@ -79,7 +76,7 @@ export type UpdateSessionCommand = {
export async function sendPassword(command: UpdateSessionCommand): Promise<{ error: string } | { redirect: string }> {
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const { serviceConfig } = getServiceConfig(_headers);
const t = await getTranslations("password");
let sessionCookie = await getSessionCookieByLoginName({
@@ -94,11 +91,7 @@ export async function sendPassword(command: UpdateSessionCommand): Promise<{ err
let loginSettings: LoginSettings | undefined;
if (!sessionCookie) {
const users = await listUsers({
serviceUrl,
loginName: command.loginName,
organizationId: command.organization,
});
const users = await listUsers({ serviceConfig, loginName: command.loginName, organizationId: command.organization });
if (users.details?.totalResult == BigInt(1) && users.result[0].userId) {
user = users.result[0];
@@ -108,10 +101,7 @@ export async function sendPassword(command: UpdateSessionCommand): Promise<{ err
password: { password: command.checks.password?.password },
});
loginSettings = await getLoginSettings({
serviceUrl,
organization: command.organization,
});
loginSettings = await getLoginSettings({ serviceConfig, organization: command.organization });
try {
session = await createSessionAndUpdateCookie({
@@ -121,10 +111,7 @@ export async function sendPassword(command: UpdateSessionCommand): Promise<{ err
});
} catch (error: any) {
if ("failedAttempts" in error && error.failedAttempts) {
const lockoutSettings = await getLockoutSettings({
serviceUrl,
orgId: command.organization,
});
const lockoutSettings = await getLockoutSettings({ serviceConfig, orgId: command.organization });
const hasLimit =
lockoutSettings?.maxPasswordAttempts !== undefined && lockoutSettings?.maxPasswordAttempts > BigInt(0);
@@ -149,10 +136,7 @@ export async function sendPassword(command: UpdateSessionCommand): Promise<{ err
// this is a fake error message to hide that the user does not even exist
return { error: t("errors.couldNotVerifyPassword") };
} else {
loginSettings = await getLoginSettings({
serviceUrl,
organization: sessionCookie.organization,
});
loginSettings = await getLoginSettings({ serviceConfig, organization: sessionCookie.organization });
if (!loginSettings) {
return { error: "Could not load login settings" };
@@ -177,10 +161,7 @@ export async function sendPassword(command: UpdateSessionCommand): Promise<{ err
});
} catch (error: any) {
if ("failedAttempts" in error && error.failedAttempts) {
const lockoutSettings = await getLockoutSettings({
serviceUrl,
orgId: command.organization,
});
const lockoutSettings = await getLockoutSettings({ serviceConfig, orgId: command.organization });
const hasLimit =
lockoutSettings?.maxPasswordAttempts !== undefined && lockoutSettings?.maxPasswordAttempts > BigInt(0);
@@ -202,10 +183,7 @@ export async function sendPassword(command: UpdateSessionCommand): Promise<{ err
return { error: t("errors.couldNotCreateSessionForUser") };
}
const userResponse = await getUserByID({
serviceUrl,
userId: session?.factors?.user?.id,
});
const userResponse = await getUserByID({ serviceConfig, userId: session?.factors?.user?.id });
if (!userResponse.user) {
return { error: t("errors.userNotFound") };
@@ -216,7 +194,7 @@ export async function sendPassword(command: UpdateSessionCommand): Promise<{ err
if (!loginSettings) {
loginSettings = await getLoginSettings({
serviceUrl,
serviceConfig,
organization: command.organization ?? session.factors?.user?.organizationId,
});
}
@@ -228,7 +206,7 @@ export async function sendPassword(command: UpdateSessionCommand): Promise<{ err
const humanUser = user.type.case === "human" ? user.type.value : undefined;
const expirySettings = await getPasswordExpirySettings({
serviceUrl,
serviceConfig,
orgId: command.organization ?? session.factors?.user?.organizationId,
});
@@ -260,10 +238,7 @@ export async function sendPassword(command: UpdateSessionCommand): Promise<{ err
// if password, check if user has MFA methods
let authMethods;
if (command.checks && command.checks.password && session.factors?.user?.id) {
const response = await listAuthenticationMethodTypes({
serviceUrl,
userId: session.factors.user.id,
});
const response = await listAuthenticationMethodTypes({ serviceConfig, userId: session.factors.user.id });
if (response.authMethodTypes && response.authMethodTypes.length) {
authMethods = response.authMethodTypes;
}
@@ -274,7 +249,7 @@ export async function sendPassword(command: UpdateSessionCommand): Promise<{ err
}
const mfaFactorCheck = await checkMFAFactors(
serviceUrl,
serviceConfig,
session,
loginSettings,
authMethods,
@@ -331,14 +306,11 @@ export async function sendPassword(command: UpdateSessionCommand): Promise<{ err
// this function lets users with code set a password or users with valid User Verification Check
export async function changePassword(command: { code?: string; userId: string; password: string }) {
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const { serviceConfig } = getServiceConfig(_headers);
const t = await getTranslations("password");
// check for init state
const { user } = await getUserByID({
serviceUrl,
userId: command.userId,
});
const { user } = await getUserByID({ serviceConfig, userId: command.userId });
if (!user || user.userId !== command.userId) {
return { error: t("errors.couldNotSendResetLink") };
@@ -351,10 +323,7 @@ export async function changePassword(command: { code?: string; userId: string; p
// check if the user has no password set in order to set a password
if (!command.code) {
const authmethods = await listAuthenticationMethodTypes({
serviceUrl,
userId,
});
const authmethods = await listAuthenticationMethodTypes({ serviceConfig, userId });
// if the user has no authmethods set, we need to check if the user was verified
if (authmethods.authMethodTypes.length !== 0) {
@@ -371,12 +340,7 @@ export async function changePassword(command: { code?: string; userId: string; p
}
}
return setUserPassword({
serviceUrl,
userId,
password: command.password,
code: command.code,
});
return setUserPassword({ serviceConfig, userId, password: command.password, code: command.code });
}
type CheckSessionAndSetPasswordCommand = {
@@ -386,7 +350,7 @@ type CheckSessionAndSetPasswordCommand = {
export async function checkSessionAndSetPassword({ sessionId, password }: CheckSessionAndSetPasswordCommand) {
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const { serviceConfig } = getServiceConfig(_headers);
const t = await getTranslations("password");
let sessionCookie;
@@ -400,7 +364,7 @@ export async function checkSessionAndSetPassword({ sessionId, password }: CheckS
let session;
try {
const sessionResponse = await getSession({
serviceUrl,
serviceConfig,
sessionId: sessionCookie.id,
sessionToken: sessionCookie.token,
});
@@ -424,10 +388,7 @@ export async function checkSessionAndSetPassword({ sessionId, password }: CheckS
// check if the user has no password set in order to set a password
let authmethods;
try {
authmethods = await listAuthenticationMethodTypes({
serviceUrl,
userId: session.factors.user.id,
});
authmethods = await listAuthenticationMethodTypes({ serviceConfig, userId: session.factors.user.id });
} catch (error) {
console.error("Error getting auth methods:", error);
return { error: "Could not load auth methods" };
@@ -439,10 +400,7 @@ export async function checkSessionAndSetPassword({ sessionId, password }: CheckS
let loginSettings;
try {
loginSettings = await getLoginSettings({
serviceUrl,
organization: session.factors.user.organizationId,
});
loginSettings = await getLoginSettings({ serviceConfig, organization: session.factors.user.organizationId });
} catch (error) {
console.error("Error getting login settings:", error);
return { error: "Could not load login settings" };
@@ -453,7 +411,7 @@ export async function checkSessionAndSetPassword({ sessionId, password }: CheckS
// if the user has no MFA but MFA is enforced, we can set a password otherwise we use the token of the user
if (forceMfa) {
console.log("Set password using service account due to enforced MFA without existing MFA methods");
return setPassword({ serviceUrl, payload }).catch((error) => {
return setPassword({ serviceConfig, payload }).catch((error) => {
// throw error if failed precondition (ex. User is not yet initialized)
if (error.code === 9 && error.message) {
return { error: t("errors.failedPrecondition") };
@@ -461,16 +419,16 @@ export async function checkSessionAndSetPassword({ sessionId, password }: CheckS
return { error: "Could not set password" };
});
} else {
const transport = async (serviceUrl: string, token: string) => {
return createServerTransport(token, serviceUrl);
const transport = async (serviceConfig: ServiceConfig, token: string) => {
return createServerTransport(token, serviceConfig);
};
const myUserService = async (serviceUrl: string, sessionToken: string) => {
const transportPromise = await transport(serviceUrl, sessionToken);
const myUserService = async (serviceConfig: ServiceConfig, sessionToken: string) => {
const transportPromise = await transport(serviceConfig, sessionToken);
return createUserServiceClient(transportPromise);
};
const selfService = await myUserService(serviceUrl, `${sessionCookie.token}`);
const selfService = await myUserService(serviceConfig, sessionCookie.token);
return selfService
.setPassword(
+12 -26
View File
@@ -7,7 +7,7 @@ import { Factors } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { ChecksJson, ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { cookies, headers } from "next/headers";
import { getTranslations } from "next-intl/server";
import { getServiceUrlFromHeaders } from "../service-url";
import { getServiceConfig } from "../service-url";
import { checkEmailVerification, checkMFAFactors } from "../verify-helper";
import { getOrSetFingerprintId } from "../fingerprint";
import crypto from "crypto";
@@ -30,11 +30,9 @@ export type RegisterUserResponse = {
export async function registerUser(command: RegisterUserCommand) {
const t = await getTranslations("register");
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const { serviceConfig } = getServiceConfig(_headers);
const addResponse = await addHumanUser({
serviceUrl,
email: command.email,
const addResponse = await addHumanUser({ serviceConfig, email: command.email,
firstName: command.firstName,
lastName: command.lastName,
password: command.password ? command.password : undefined,
@@ -45,9 +43,7 @@ export async function registerUser(command: RegisterUserCommand) {
return { error: t("errors.couldNotCreateUser") };
}
const loginSettings = await getLoginSettings({
serviceUrl,
organization: command.organization,
const loginSettings = await getLoginSettings({ serviceConfig, organization: command.organization,
});
let checkPayload: any = {
@@ -100,9 +96,7 @@ export async function registerUser(command: RegisterUserCommand) {
return { redirect: "/passkey/set?" + params };
} else {
const userResponse = await getUserByID({
serviceUrl,
userId: session?.factors?.user?.id,
const userResponse = await getUserByID({ serviceConfig, userId: session?.factors?.user?.id,
});
if (!userResponse.user) {
@@ -162,11 +156,9 @@ export async function registerUserAndLinkToIDP(command: RegisterUserAndLinkToIDP
const t = await getTranslations("register");
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const { serviceConfig } = getServiceConfig(_headers);
const addUserResponse = await addHumanUser({
serviceUrl,
email: command.email,
const addUserResponse = await addHumanUser({ serviceConfig, email: command.email,
firstName: command.firstName,
lastName: command.lastName,
organization: command.organization,
@@ -176,14 +168,10 @@ export async function registerUserAndLinkToIDP(command: RegisterUserAndLinkToIDP
return { error: t("errors.couldNotCreateUser") };
}
const loginSettings = await getLoginSettings({
serviceUrl,
organization: command.organization,
const loginSettings = await getLoginSettings({ serviceConfig, organization: command.organization,
});
const idpLink = await addIDPLink({
serviceUrl,
idp: {
const idpLink = await addIDPLink({ serviceConfig, idp: {
id: command.idpId,
userId: command.idpUserId,
userName: command.idpUserName,
@@ -207,7 +195,7 @@ export async function registerUserAndLinkToIDP(command: RegisterUserAndLinkToIDP
}
// const userResponse = await getUserByID({
// serviceUrl,
// serviceConfig.baseUrl,
// userId: session?.factors?.user?.id,
// });
@@ -227,9 +215,7 @@ export async function registerUserAndLinkToIDP(command: RegisterUserAndLinkToIDP
// check if user has MFA methods
let authMethods;
if (session.factors?.user?.id) {
const response = await listAuthenticationMethodTypes({
serviceUrl,
userId: session.factors.user.id,
const response = await listAuthenticationMethodTypes({ serviceConfig, userId: session.factors.user.id,
});
if (response.authMethodTypes && response.authMethodTypes.length) {
authMethods = response.authMethodTypes;
@@ -239,7 +225,7 @@ export async function registerUserAndLinkToIDP(command: RegisterUserAndLinkToIDP
// Always check MFA factors, even if no auth methods are configured
// This ensures that force MFA settings are respected
const mfaFactorCheck = await checkMFAFactors(
serviceUrl,
serviceConfig,
session,
loginSettings,
authMethods || [], // Pass empty array if no auth methods
+14 -26
View File
@@ -21,8 +21,8 @@ import {
getSessionCookieByLoginName,
removeSessionFromCookie,
} from "../cookies";
import { getServiceUrlFromHeaders } from "../service-url";
import { getOriginalHost } from "./host";
import { getServiceConfig } from "../service-url";
import { getPublicHost } from "./host";
export async function skipMFAAndContinueWithNextUrl({
userId,
@@ -38,14 +38,11 @@ export async function skipMFAAndContinueWithNextUrl({
organization?: string;
}): Promise<{ redirect: string } | { error: string }> {
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const { serviceConfig } = getServiceConfig(_headers);
const loginSettings = await getLoginSettings({
serviceUrl,
organization: organization,
});
const loginSettings = await getLoginSettings({ serviceConfig, organization: organization });
await humanMFAInitSkipped({ serviceUrl, userId });
await humanMFAInitSkipped({ serviceConfig, userId });
if (requestId && sessionId) {
return completeFlowOrGetUrl(
@@ -73,14 +70,11 @@ export type ContinueWithSessionCommand = Session & { requestId?: string };
export async function continueWithSession({ requestId, ...session }: ContinueWithSessionCommand) {
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const { serviceConfig } = getServiceConfig(_headers);
const t = await getTranslations("error");
const loginSettings = await getLoginSettings({
serviceUrl,
organization: session.factors?.user?.organizationId,
});
const loginSettings = await getLoginSettings({ serviceConfig, organization: session.factors?.user?.organizationId });
if (requestId && session.id && session.factors?.user) {
return completeFlowOrGetUrl(
@@ -130,8 +124,8 @@ export async function updateSession(options: UpdateSessionCommand) {
}
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const host = await getOriginalHost();
const { serviceConfig } = getServiceConfig(_headers);
const host = getPublicHost(_headers);
if (!host) {
return { error: "Could not get host" };
@@ -143,10 +137,7 @@ export async function updateSession(options: UpdateSessionCommand) {
challenges.webAuthN.domain = hostname;
}
const loginSettings = await getLoginSettings({
serviceUrl,
organization,
});
const loginSettings = await getLoginSettings({ serviceConfig, organization });
let lifetime = checks?.webAuthN
? loginSettings?.multiFactorCheckLifetime // TODO different lifetime for webauthn u2f/passkey
@@ -177,10 +168,7 @@ export async function updateSession(options: UpdateSessionCommand) {
// if password, check if user has MFA methods
let authMethods;
if (checks && checks.password && session.factors?.user?.id) {
const response = await listAuthenticationMethodTypes({
serviceUrl,
userId: session.factors.user.id,
});
const response = await listAuthenticationMethodTypes({ serviceConfig, userId: session.factors.user.id });
if (response.authMethodTypes && response.authMethodTypes.length) {
authMethods = response.authMethodTypes;
}
@@ -200,19 +188,19 @@ type ClearSessionOptions = {
export async function clearSession(options: ClearSessionOptions) {
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const { serviceConfig } = getServiceConfig(_headers);
const { sessionId } = options;
const sessionCookie = await getSessionCookieById({ sessionId });
const deleteResponse = await deleteSession({
serviceUrl,
serviceConfig,
sessionId: sessionCookie.id,
sessionToken: sessionCookie.token,
});
const securitySettings = await getSecuritySettings({ serviceUrl });
const securitySettings = await getSecuritySettings({ serviceConfig });
const iFrameEnabled = !!securitySettings?.embeddedIframe?.enabled;
if (!deleteResponse) {
+9 -17
View File
@@ -6,8 +6,8 @@ import { VerifyU2FRegistrationRequestSchema } from "@zitadel/proto/zitadel/user/
import { headers } from "next/headers";
import { userAgent } from "next/server";
import { getSessionCookieById } from "../cookies";
import { getServiceUrlFromHeaders } from "../service-url";
import { getOriginalHost } from "./host";
import { getServiceConfig } from "../service-url";
import { getPublicHost } from "./host";
type RegisterU2FCommand = {
sessionId: string;
@@ -22,8 +22,8 @@ type VerifyU2FCommand = {
export async function addU2F(command: RegisterU2FCommand) {
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const host = await getOriginalHost();
const { serviceConfig } = getServiceConfig(_headers);
const host = getPublicHost(_headers);
const sessionCookie = await getSessionCookieById({
sessionId: command.sessionId,
@@ -33,11 +33,7 @@ export async function addU2F(command: RegisterU2FCommand) {
return { error: "Could not get session" };
}
const session = await getSession({
serviceUrl,
sessionId: sessionCookie.id,
sessionToken: sessionCookie.token,
});
const session = await getSession({ serviceConfig, sessionId: sessionCookie.id, sessionToken: sessionCookie.token });
const [hostname] = host.split(":");
@@ -51,12 +47,12 @@ export async function addU2F(command: RegisterU2FCommand) {
return { error: "Could not get session" };
}
return registerU2F({ serviceUrl, userId, domain: hostname });
return registerU2F({ serviceConfig, userId, domain: hostname });
}
export async function verifyU2F(command: VerifyU2FCommand) {
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const { serviceConfig } = getServiceConfig(_headers);
let passkeyName = command.passkeyName;
if (!passkeyName) {
const headersList = await headers();
@@ -71,11 +67,7 @@ export async function verifyU2F(command: VerifyU2FCommand) {
sessionId: command.sessionId,
});
const session = await getSession({
serviceUrl,
sessionId: sessionCookie.id,
sessionToken: sessionCookie.token,
});
const session = await getSession({ serviceConfig, sessionId: sessionCookie.id, sessionToken: sessionCookie.token });
const userId = session?.session?.factors?.user?.id;
@@ -90,5 +82,5 @@ export async function verifyU2F(command: VerifyU2FCommand) {
userId,
});
return verifyU2FRegistration({ serviceUrl, request });
return verifyU2FRegistration({ serviceConfig, request });
}
+27 -58
View File
@@ -20,30 +20,26 @@ import { cookies, headers } from "next/headers";
import { completeFlowOrGetUrl } from "../client";
import { getSessionCookieByLoginName } from "../cookies";
import { getOrSetFingerprintId } from "../fingerprint";
import { getServiceUrlFromHeaders } from "../service-url";
import { getServiceConfig } from "../service-url";
import { loadMostRecentSession } from "../session";
import { checkMFAFactors } from "../verify-helper";
import { createSessionAndUpdateCookie } from "./cookie";
import { getOriginalHostWithProtocol } from "./host";
import { getPublicHostWithProtocol } from "./host";
import { getTranslations } from "next-intl/server";
export async function verifyTOTP(code: string, loginName?: string, organization?: string) {
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const { serviceConfig } = getServiceConfig(_headers);
return loadMostRecentSession({
serviceUrl,
serviceConfig,
sessionParams: {
loginName,
organization,
},
}).then((session) => {
if (session?.factors?.user?.id) {
return verifyTOTPRegistration({
serviceUrl,
code,
userId: session.factors.user.id,
});
return verifyTOTPRegistration({ serviceConfig, code, userId: session.factors.user.id });
} else {
throw Error("No user id found in session.");
}
@@ -62,22 +58,14 @@ type VerifyUserByEmailCommand = {
export async function sendVerification(command: VerifyUserByEmailCommand) {
const t = await getTranslations("verify");
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const { serviceConfig } = getServiceConfig(_headers);
const verifyResponse = command.isInvite
? await verifyInviteCode({
serviceUrl,
userId: command.userId,
verificationCode: command.code,
}).catch((error) => {
? await verifyInviteCode({ serviceConfig, userId: command.userId, verificationCode: command.code }).catch((error) => {
console.warn(error);
return { error: t("errors.couldNotVerifyInvite") };
})
: await verifyEmail({
serviceUrl,
userId: command.userId,
verificationCode: command.code,
}).catch((error) => {
: await verifyEmail({ serviceConfig, userId: command.userId, verificationCode: command.code }).catch((error) => {
console.warn(error);
return { error: t("errors.couldNotVerifyEmail") };
});
@@ -91,10 +79,7 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
}
let session: Session | undefined;
const userResponse = await getUserByID({
serviceUrl,
userId: command.userId,
});
const userResponse = await getUserByID({ serviceConfig, userId: command.userId });
if (!userResponse || !userResponse.user) {
return { error: t("errors.couldNotLoadUser") };
@@ -110,22 +95,17 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
});
if (sessionCookie) {
session = await getSession({
serviceUrl,
sessionId: sessionCookie.id,
sessionToken: sessionCookie.token,
}).then((response) => {
if (response?.session) {
return response.session;
}
});
session = await getSession({ serviceConfig, sessionId: sessionCookie.id, sessionToken: sessionCookie.token }).then(
(response) => {
if (response?.session) {
return response.session;
}
},
);
}
// load auth methods for user
const authMethodResponse = await listAuthenticationMethodTypes({
serviceUrl,
userId: user.userId,
});
const authMethodResponse = await listAuthenticationMethodTypes({ serviceConfig, userId: user.userId });
if (!authMethodResponse || !authMethodResponse.authMethodTypes) {
return { error: t("errors.couldNotLoadAuthenticators") };
@@ -203,14 +183,11 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
return { redirect: `/verify/success?${verifySuccessParams}` };
}
const loginSettings = await getLoginSettings({
serviceUrl,
organization: user.details?.resourceOwner,
});
const loginSettings = await getLoginSettings({ serviceConfig, organization: user.details?.resourceOwner });
// redirect to mfa factor if user has one, or redirect to set one up
const mfaFactorCheck = await checkMFAFactors(
serviceUrl,
serviceConfig,
session,
loginSettings,
authMethodResponse.authMethodTypes,
@@ -253,14 +230,14 @@ type resendVerifyEmailCommand = {
export async function resendVerification(command: resendVerifyEmailCommand) {
const t = await getTranslations("verify");
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const hostWithProtocol = await getOriginalHostWithProtocol();
const { serviceConfig } = getServiceConfig(_headers);
const hostWithProtocol = await getPublicHostWithProtocol(_headers);
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
return command.isInvite
? createInviteCode({
serviceUrl,
serviceConfig,
userId: command.userId,
urlTemplate:
`${hostWithProtocol}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` +
@@ -272,8 +249,8 @@ export async function resendVerification(command: resendVerifyEmailCommand) {
return { error: t("errors.couldNotResendInvite") };
})
: zitadelSendEmailCode({
serviceConfig,
userId: command.userId,
serviceUrl,
urlTemplate:
`${hostWithProtocol}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` +
(command.requestId ? `&requestId=${command.requestId}` : ""),
@@ -287,22 +264,14 @@ type SendEmailCommand = {
export async function sendEmailCode(command: SendEmailCommand) {
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const { serviceConfig } = getServiceConfig(_headers);
return zitadelSendEmailCode({
serviceUrl,
userId: command.userId,
urlTemplate: command.urlTemplate,
});
return zitadelSendEmailCode({ serviceConfig, userId: command.userId, urlTemplate: command.urlTemplate });
}
export async function sendInviteEmailCode(command: SendEmailCommand) {
const _headers = await headers();
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const { serviceConfig } = getServiceConfig(_headers);
return createInviteCode({
serviceUrl,
userId: command.userId,
urlTemplate: command.urlTemplate,
});
return createInviteCode({ serviceConfig, userId: command.userId, urlTemplate: command.urlTemplate });
}