mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-22 23:42:16 +00:00
* add mfa check to session validity check * change session check to check for already set up mfa methods * lint * improve naming * test changes * cleanup continueWithSession fcn
318 lines
9.0 KiB
TypeScript
318 lines
9.0 KiB
TypeScript
"use server";
|
|
|
|
import { isClassifiedError } from "@/lib/grpc/interceptors/error-classification";
|
|
import { createLogger } from "@/lib/logger";
|
|
import { createSessionAndUpdateCookie, setSessionAndUpdateCookie } from "@/lib/server/cookie";
|
|
import {
|
|
deleteSession,
|
|
getLoginSettings,
|
|
getSecuritySettings,
|
|
humanMFAInitSkipped,
|
|
listAuthenticationMethodTypes,
|
|
listUsers,
|
|
} from "@/lib/zitadel";
|
|
import { create, Duration } from "@zitadel/client";
|
|
import { RequestChallenges } from "@zitadel/proto/zitadel/session/v2/challenge_pb";
|
|
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
|
import { Checks, ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
|
import { getTranslations } from "next-intl/server";
|
|
import { headers } from "next/headers";
|
|
import { completeFlowOrGetUrl } from "../client";
|
|
import {
|
|
getMostRecentSessionCookie,
|
|
getSessionCookieById,
|
|
getSessionCookieByLoginName,
|
|
removeSessionFromCookie,
|
|
} from "../cookies";
|
|
import { getServiceConfig } from "../service-url";
|
|
import { isSessionValid } from "../session";
|
|
import { getPublicHost } from "./host";
|
|
import { sendLoginname } from "./loginname";
|
|
|
|
const logger = createLogger("session");
|
|
|
|
export async function skipMFAAndContinueWithNextUrl({
|
|
userId,
|
|
requestId,
|
|
loginName,
|
|
sessionId,
|
|
organization,
|
|
}: {
|
|
userId: string;
|
|
loginName?: string;
|
|
sessionId?: string;
|
|
requestId?: string;
|
|
organization?: string;
|
|
}): Promise<{ redirect: string } | { error: string } | { samlData: { url: string; fields: Record<string, string> } }> {
|
|
const _headers = await headers();
|
|
const { serviceConfig } = getServiceConfig(_headers);
|
|
|
|
const loginSettings = await getLoginSettings({ serviceConfig, organization: organization });
|
|
|
|
await humanMFAInitSkipped({ serviceConfig, userId });
|
|
|
|
if (requestId && sessionId) {
|
|
return completeFlowOrGetUrl(
|
|
{
|
|
sessionId: sessionId,
|
|
requestId: requestId,
|
|
organization: organization,
|
|
},
|
|
loginSettings?.defaultRedirectUri,
|
|
);
|
|
} else if (loginName) {
|
|
return completeFlowOrGetUrl(
|
|
{
|
|
loginName: loginName,
|
|
organization: organization,
|
|
},
|
|
loginSettings?.defaultRedirectUri,
|
|
);
|
|
}
|
|
|
|
return { error: "Could not skip MFA and continue" };
|
|
}
|
|
|
|
export type ContinueWithSessionCommand = Session & { requestId?: string };
|
|
|
|
export async function continueWithSession({ requestId, ...session }: ContinueWithSessionCommand) {
|
|
const _headers = await headers();
|
|
const { serviceConfig } = getServiceConfig(_headers);
|
|
|
|
const t = await getTranslations("error");
|
|
|
|
if (!session.factors?.user) {
|
|
return { error: t("couldNotContinueSession") };
|
|
}
|
|
|
|
const loginSettings = await getLoginSettings({ serviceConfig, organization: session.factors.user.organizationId });
|
|
|
|
// Validate session (including MFA) before completing the flow
|
|
const valid = await isSessionValid({ serviceConfig, session: session as Session });
|
|
|
|
if (!valid) {
|
|
logger.warn("continueWithSession: session is not valid (e.g. MFA not completed), redirecting to re-authenticate", {
|
|
sessionId: session.id,
|
|
});
|
|
|
|
// Redirect user to re-authenticate (will route to MFA page if password is still valid)
|
|
const res = await sendLoginname({
|
|
loginName: session.factors.user.loginName,
|
|
organization: session.factors.user.organizationId,
|
|
requestId: requestId,
|
|
});
|
|
|
|
if (res && "redirect" in res && res.redirect) {
|
|
return { redirect: res.redirect };
|
|
}
|
|
|
|
if (res && "samlData" in res && res.samlData) {
|
|
return { samlData: res.samlData };
|
|
}
|
|
|
|
return { error: t("couldNotContinueSession") };
|
|
}
|
|
|
|
if (requestId && session.id) {
|
|
return completeFlowOrGetUrl(
|
|
{
|
|
sessionId: session.id,
|
|
requestId: requestId,
|
|
organization: session.factors.user.organizationId,
|
|
},
|
|
loginSettings?.defaultRedirectUri,
|
|
);
|
|
}
|
|
|
|
return completeFlowOrGetUrl(
|
|
{
|
|
loginName: session.factors.user.loginName,
|
|
organization: session.factors.user.organizationId,
|
|
},
|
|
loginSettings?.defaultRedirectUri,
|
|
);
|
|
}
|
|
|
|
export type UpdateSessionCommand = {
|
|
loginName?: string;
|
|
sessionId?: string;
|
|
organization?: string;
|
|
checks?: Checks;
|
|
requestId?: string;
|
|
challenges?: RequestChallenges;
|
|
lifetime?: Duration;
|
|
};
|
|
|
|
export async function updateOrCreateSession(options: UpdateSessionCommand) {
|
|
let { loginName, sessionId, organization, checks, requestId, challenges, lifetime } = options;
|
|
|
|
const _headers = await headers();
|
|
const { serviceConfig } = getServiceConfig(_headers);
|
|
const host = getPublicHost(_headers);
|
|
|
|
const t = await getTranslations("verify.errors");
|
|
|
|
if (!host) {
|
|
return { error: "Could not get host" }; // Technical error, maybe leave or translate if key exists
|
|
}
|
|
|
|
if (challenges && challenges.webAuthN && !challenges.webAuthN.domain) {
|
|
const [hostname] = host.split(":");
|
|
|
|
challenges.webAuthN.domain = hostname;
|
|
}
|
|
|
|
let recentSession = sessionId
|
|
? await getSessionCookieById({ sessionId })
|
|
: loginName
|
|
? await getSessionCookieByLoginName({ loginName, organization })
|
|
: await getMostRecentSessionCookie();
|
|
|
|
if (!recentSession) {
|
|
if (!loginName) {
|
|
return { error: t("couldNotFindSession") };
|
|
}
|
|
|
|
const checks = create(ChecksSchema, {
|
|
user: { search: { case: "loginName", value: loginName } },
|
|
});
|
|
|
|
const result = await createSessionAndUpdateCookie({
|
|
checks,
|
|
challenges,
|
|
requestId,
|
|
}).catch((error) => {
|
|
if (isClassifiedError(error) && error.isUserError) {
|
|
logger.warn("Could not create session (client error)", { grpcCode: error.code, httpStatus: error.httpStatus });
|
|
} else {
|
|
logger.error("Could not create session (server error)", { error });
|
|
}
|
|
return undefined;
|
|
});
|
|
|
|
if (result && "sessionCookie" in result) {
|
|
recentSession = result.sessionCookie;
|
|
}
|
|
|
|
if (!recentSession) {
|
|
return {
|
|
error: t("couldNotFindSession"),
|
|
};
|
|
}
|
|
}
|
|
|
|
const loginSettings = await getLoginSettings({ serviceConfig, organization });
|
|
|
|
if (!lifetime) {
|
|
lifetime = checks?.webAuthN
|
|
? loginSettings?.multiFactorCheckLifetime // TODO different lifetime for webauthn u2f/passkey
|
|
: checks?.otpEmail || checks?.otpSms
|
|
? loginSettings?.secondFactorCheckLifetime
|
|
: undefined;
|
|
}
|
|
|
|
if (!lifetime || !lifetime.seconds) {
|
|
logger.warn("No lifetime provided for session, defaulting to 24 hours");
|
|
lifetime = {
|
|
seconds: BigInt(60 * 60 * 24), // default to 24 hours
|
|
nanos: 0,
|
|
} as Duration;
|
|
}
|
|
|
|
let session;
|
|
try {
|
|
session = await setSessionAndUpdateCookie({
|
|
recentCookie: recentSession,
|
|
checks,
|
|
challenges,
|
|
requestId,
|
|
lifetime,
|
|
});
|
|
} catch (error) {
|
|
const loginNameForCreation = options.loginName || recentSession?.loginName;
|
|
const orgForCreation = options.organization || recentSession?.organization;
|
|
|
|
if (!loginNameForCreation) {
|
|
throw error;
|
|
}
|
|
|
|
const users = await listUsers({
|
|
serviceConfig,
|
|
loginName: loginNameForCreation,
|
|
organizationId: orgForCreation,
|
|
});
|
|
|
|
if (users.details?.totalResult === BigInt(1) && users.result[0].userId) {
|
|
const user = users.result[0];
|
|
const newChecks = create(ChecksSchema, {
|
|
...(checks || {}),
|
|
user: { search: { case: "userId", value: user.userId } } as any,
|
|
});
|
|
|
|
const result = await createSessionAndUpdateCookie({
|
|
checks: newChecks,
|
|
requestId,
|
|
lifetime,
|
|
challenges,
|
|
});
|
|
// @ts-ignore
|
|
session = { ...result.session, challenges: result.challenges };
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
if (!session || ("error" in session && session.error)) {
|
|
return { error: t("couldNotUpdateSession") };
|
|
}
|
|
|
|
// if password, check if user has MFA methods
|
|
let authMethods;
|
|
if (checks && checks.password && session.factors?.user?.id) {
|
|
const response = await listAuthenticationMethodTypes({ serviceConfig, userId: session.factors.user.id });
|
|
if (response.authMethodTypes && response.authMethodTypes.length) {
|
|
authMethods = response.authMethodTypes;
|
|
}
|
|
}
|
|
|
|
return {
|
|
sessionId: session.id,
|
|
factors: session.factors,
|
|
// @ts-ignore
|
|
challenges: session.challenges,
|
|
authMethods,
|
|
};
|
|
}
|
|
|
|
type ClearSessionOptions = {
|
|
sessionId: string;
|
|
};
|
|
|
|
export async function clearSession(options: ClearSessionOptions) {
|
|
const _headers = await headers();
|
|
const { serviceConfig } = getServiceConfig(_headers);
|
|
|
|
const { sessionId } = options;
|
|
|
|
const sessionCookie = await getSessionCookieById({ sessionId });
|
|
|
|
if (!sessionCookie) {
|
|
return;
|
|
}
|
|
|
|
const deleteResponse = await deleteSession({
|
|
serviceConfig,
|
|
sessionId: sessionCookie.id,
|
|
sessionToken: sessionCookie.token,
|
|
});
|
|
|
|
const securitySettings = await getSecuritySettings({ serviceConfig });
|
|
const iFrameEnabled = !!securitySettings?.embeddedIframe?.enabled;
|
|
|
|
if (!deleteResponse) {
|
|
throw new Error("Could not delete session");
|
|
}
|
|
|
|
return removeSessionFromCookie({ session: sessionCookie, iFrameEnabled });
|
|
}
|