mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-03 04:42:15 +00:00
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:
co-authored by
Livio Spring
parent
f9e3871bb3
commit
df75be96ff
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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") };
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user