"use server"; import { getLoginSettings, getUserByID, listAuthenticationMethodTypes, ServiceConfig, startIdentityProviderFlow, startLDAPIdentityProviderFlow, } from "@/lib/zitadel"; import crypto from "crypto"; import { headers } from "next/headers"; import { redirect } from "next/navigation"; import { completeFlowOrGetUrl } from "../client"; import { getSessionCookieById } from "../cookies"; import { getOrSetFingerprintId } from "../fingerprint"; import { getServiceConfig } from "../service-url"; import { checkEmailVerification, checkMFAFactors } from "../verify-helper"; import { createSessionForIdpAndUpdateCookie } from "./cookie"; import { getPublicHost } from "./host"; export type RedirectToIdpState = { error?: string | null; samlData?: { url: string; fields: Record } } | undefined; export async function redirectToIdp(prevState: RedirectToIdpState, formData: FormData): Promise { const _headers = await headers(); const { serviceConfig } = getServiceConfig(_headers); const host = getPublicHost(_headers); const params = new URLSearchParams(); const sessionId = formData.get("sessionId") as string; const requestId = formData.get("requestId") as string; const organization = formData.get("organization") as string; const idpId = formData.get("id") as string; const provider = formData.get("provider") as string; const postErrorRedirectUrl = formData.get("postErrorRedirectUrl") as string; const loginHint = formData.get("loginHint") as string; if (sessionId) { try { // Validate that the requestor owns the session they are trying to link await getSessionCookieById({ sessionId }); // Get fingerprint (ensure it exists) const fingerprintId = await getOrSetFingerprintId(); // Create hash to verify intent upon return const linkFingerprint = crypto .createHash("sha256") .update(sessionId + fingerprintId) .digest("hex"); params.set("linkToSessionId", sessionId); params.set("linkFingerprint", linkFingerprint); } catch { return { error: "Invalid session for linking" }; } } if (requestId) params.set("requestId", requestId); if (organization) params.set("organization", organization); if (postErrorRedirectUrl) params.set("postErrorRedirectUrl", postErrorRedirectUrl); // redirect to LDAP page where username and password is requested if (provider === "ldap") { params.set("idpId", idpId); redirect(`/idp/ldap?` + params.toString()); } const response = await startIDPFlow({ serviceConfig, host, idpId, successUrl: `/idp/${provider}/process?` + params.toString(), failureUrl: `/idp/${provider}/failure?` + params.toString(), loginHint: loginHint || undefined, }); if (!response) { return { error: "Could not start IDP flow" }; } if (response && "error" in response && response?.error) { return { error: response.error }; } if (response && "redirect" in response && response?.redirect) { redirect(response.redirect); } if (response && "samlData" in response && response?.samlData) { return { samlData: response.samlData }; } return { error: "Unexpected response from IDP flow" }; } export type StartIDPFlowCommand = { serviceConfig: ServiceConfig; host: string; idpId: string; successUrl: string; failureUrl: string; loginHint?: string; }; async function startIDPFlow(command: StartIDPFlowCommand) { const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? ""; const response = await startIdentityProviderFlow({ serviceConfig: command.serviceConfig, idpId: command.idpId, urls: { successUrl: `${command.host.includes("localhost") ? "http://" : "https://"}${command.host}${basePath}${command.successUrl}`, failureUrl: `${command.host.includes("localhost") ? "http://" : "https://"}${command.host}${basePath}${command.failureUrl}`, loginHint: command.loginHint, }, }); if (!response || !response.url) { return { error: "Could not start IDP flow" }; } if (response.fields) { return { samlData: { url: response.url, fields: response.fields } }; } return { redirect: response.url }; } export type CreateNewSessionCommand = { userId: string; idpIntent: { idpIntentId: string; idpIntentToken: string; }; loginName?: string; password?: string; organization?: string; requestId?: string; }; export async function createNewSessionFromIdpIntent(command: CreateNewSessionCommand) { const _headers = await headers(); const { serviceConfig } = getServiceConfig(_headers); if (!command.userId || !command.idpIntent) { throw new Error("No userId or loginName provided"); } const userResponse = await getUserByID({ serviceConfig, userId: command.userId }); if (!userResponse || !userResponse.user) { return { error: "User not found in the system" }; } const loginSettings = await getLoginSettings({ serviceConfig, organization: userResponse.user.details?.resourceOwner }); const session = await createSessionForIdpAndUpdateCookie({ userId: command.userId, idpIntent: command.idpIntent, requestId: command.requestId, lifetime: loginSettings?.externalLoginCheckLifetime, }); if (!session || !session.factors?.user) { return { error: "Could not create session" }; } const humanUser = userResponse.user.type.case === "human" ? userResponse.user.type.value : undefined; // check to see if user was verified const emailVerificationCheck = await checkEmailVerification(session, humanUser, command.organization, command.requestId); if (emailVerificationCheck?.redirect) { return emailVerificationCheck; } // check if user has MFA methods let authMethods; if (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( serviceConfig, session, loginSettings, authMethods || [], // Pass empty array if no auth methods command.organization, command.requestId, ); if (mfaFactorCheck?.redirect) { return mfaFactorCheck; } return completeFlowOrGetUrl( command.requestId && session.id ? { sessionId: session.id, requestId: command.requestId, organization: session.factors.user.organizationId, } : { loginName: session.factors.user.loginName, organization: session.factors.user.organizationId, }, loginSettings?.defaultRedirectUri, ); } type createNewSessionForLDAPCommand = { username: string; password: string; idpId: string; link: boolean; requestId?: string; organization?: string; postErrorRedirectUrl?: string; linkToSessionId?: string; linkFingerprint?: string; }; export async function createNewSessionForLDAP(command: createNewSessionForLDAPCommand) { const _headers = await headers(); const { serviceConfig } = getServiceConfig(_headers); if (!command.username || !command.password) { return { error: "No username or password provided" }; } const response = await startLDAPIdentityProviderFlow({ serviceConfig, idpId: command.idpId, username: command.username, password: command.password, }); if (!response || response.nextStep.case !== "idpIntent" || !response.nextStep.value) { return { error: "Could not start LDAP identity provider flow" }; } const { userId, idpIntentId, idpIntentToken } = response.nextStep.value; const params = new URLSearchParams({ userId, id: idpIntentId, token: idpIntentToken, }); if (command.link) { params.set("link", "true"); } if (command.requestId) params.set("requestId", command.requestId); if (command.organization) params.set("organization", command.organization); if (command.postErrorRedirectUrl) params.set("postErrorRedirectUrl", command.postErrorRedirectUrl); if (command.linkToSessionId) params.set("linkToSessionId", command.linkToSessionId); if (command.linkFingerprint) params.set("linkFingerprint", command.linkFingerprint); return { redirect: `/idp/ldap/process?` + params.toString(), }; }