mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +00:00
# Which Problems Are Solved In the login we often check if a GRPC Duration is not defined however it can also be set to 0. Using the API it's possible to set the password check lifetime to zero which broke the login v2. # How the Problems Are Solved Also check if the GRPC Duration seconds field is not 0 # Additional Context - May help if the issue here is actually accidentally setting password lifetime check to 0 using the API #10865 Co-authored-by: Max Peintner <[email protected]>
492 lines
15 KiB
TypeScript
492 lines
15 KiB
TypeScript
"use server";
|
|
|
|
import { createSessionAndUpdateCookie, setSessionAndUpdateCookie } from "@/lib/server/cookie";
|
|
import {
|
|
getLockoutSettings,
|
|
getLoginSettings,
|
|
getPasswordExpirySettings,
|
|
getSession,
|
|
getUserByID,
|
|
listAuthenticationMethodTypes,
|
|
listUsers,
|
|
passwordReset,
|
|
setPassword,
|
|
setUserPassword,
|
|
} from "@/lib/zitadel";
|
|
import { ConnectError, create, Duration } from "@zitadel/client";
|
|
import { createUserServiceClient } from "@zitadel/client/v2";
|
|
import { Checks, ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
|
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
|
|
import { User, UserState } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
|
import { SetPasswordRequestSchema } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
|
import { headers } from "next/headers";
|
|
import { completeFlowOrGetUrl } from "../client";
|
|
import { getSessionCookieById, getSessionCookieByLoginName } from "../cookies";
|
|
import { getServiceUrlFromHeaders } from "../service-url";
|
|
import { getOriginalHostWithProtocol } from "./host";
|
|
import {
|
|
checkEmailVerification,
|
|
checkMFAFactors,
|
|
checkPasswordChangeRequired,
|
|
checkUserVerification,
|
|
} from "../verify-helper";
|
|
import { createServerTransport } from "../zitadel";
|
|
import { getTranslations } from "next-intl/server";
|
|
|
|
type ResetPasswordCommand = {
|
|
loginName: string;
|
|
organization?: string;
|
|
requestId?: string;
|
|
};
|
|
|
|
export async function resetPassword(command: ResetPasswordCommand) {
|
|
const _headers = await headers();
|
|
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
|
|
|
const t = await getTranslations("password");
|
|
|
|
// Get the original host that the user sees with protocol
|
|
const hostWithProtocol = await getOriginalHostWithProtocol();
|
|
|
|
const users = await listUsers({
|
|
serviceUrl,
|
|
loginName: command.loginName,
|
|
organizationId: command.organization,
|
|
});
|
|
|
|
if (!users.details || users.details.totalResult !== BigInt(1) || !users.result[0].userId) {
|
|
return { error: t("errors.couldNotSendResetLink") };
|
|
}
|
|
const userId = users.result[0].userId;
|
|
|
|
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
|
|
|
|
return passwordReset({
|
|
serviceUrl,
|
|
userId,
|
|
urlTemplate:
|
|
`${hostWithProtocol}${basePath}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` +
|
|
(command.requestId ? `&requestId=${command.requestId}` : ""),
|
|
});
|
|
}
|
|
|
|
export type UpdateSessionCommand = {
|
|
loginName: string;
|
|
organization?: string;
|
|
checks: Checks;
|
|
requestId?: string;
|
|
};
|
|
|
|
export async function sendPassword(command: UpdateSessionCommand): Promise<{ error: string } | { redirect: string }> {
|
|
const _headers = await headers();
|
|
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
|
const t = await getTranslations("password");
|
|
|
|
let sessionCookie = await getSessionCookieByLoginName({
|
|
loginName: command.loginName,
|
|
organization: command.organization,
|
|
}).catch((error) => {
|
|
console.warn("Ignored error:", error);
|
|
});
|
|
|
|
let session;
|
|
let user: User;
|
|
let loginSettings: LoginSettings | undefined;
|
|
|
|
if (!sessionCookie) {
|
|
const users = await listUsers({
|
|
serviceUrl,
|
|
loginName: command.loginName,
|
|
organizationId: command.organization,
|
|
});
|
|
|
|
if (users.details?.totalResult == BigInt(1) && users.result[0].userId) {
|
|
user = users.result[0];
|
|
|
|
const checks = create(ChecksSchema, {
|
|
user: { search: { case: "userId", value: users.result[0].userId } },
|
|
password: { password: command.checks.password?.password },
|
|
});
|
|
|
|
loginSettings = await getLoginSettings({
|
|
serviceUrl,
|
|
organization: command.organization,
|
|
});
|
|
|
|
try {
|
|
session = await createSessionAndUpdateCookie({
|
|
checks,
|
|
requestId: command.requestId,
|
|
lifetime: loginSettings?.passwordCheckLifetime,
|
|
});
|
|
} catch (error: any) {
|
|
if ("failedAttempts" in error && error.failedAttempts) {
|
|
const lockoutSettings = await getLockoutSettings({
|
|
serviceUrl,
|
|
orgId: command.organization,
|
|
});
|
|
|
|
const hasLimit =
|
|
lockoutSettings?.maxPasswordAttempts !== undefined && lockoutSettings?.maxPasswordAttempts > BigInt(0);
|
|
const locked = hasLimit && error.failedAttempts >= lockoutSettings?.maxPasswordAttempts;
|
|
const messageKey = hasLimit ? "errors.failedToAuthenticate" : "errors.failedToAuthenticateNoLimit";
|
|
|
|
return {
|
|
error: t(messageKey, {
|
|
failedAttempts: error.failedAttempts,
|
|
maxPasswordAttempts: hasLimit ? (lockoutSettings?.maxPasswordAttempts).toString() : "?",
|
|
lockoutMessage: locked ? t("errors.accountLockedContactAdmin") : "",
|
|
}),
|
|
};
|
|
}
|
|
return { error: t("errors.couldNotCreateSessionForUser") };
|
|
}
|
|
} else {
|
|
// this is a fake error message to hide that the user does not even exist
|
|
return { error: "Could not verify password" };
|
|
}
|
|
|
|
// 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,
|
|
});
|
|
|
|
if (!loginSettings) {
|
|
return { error: "Could not load login settings" };
|
|
}
|
|
|
|
let lifetime = loginSettings.passwordCheckLifetime;
|
|
|
|
if (!lifetime || !lifetime.seconds) {
|
|
console.warn("No password lifetime provided, defaulting to 24 hours");
|
|
lifetime = {
|
|
seconds: BigInt(60 * 60 * 24), // default to 24 hours
|
|
nanos: 0,
|
|
} as Duration;
|
|
}
|
|
|
|
try {
|
|
session = await setSessionAndUpdateCookie({
|
|
recentCookie: sessionCookie,
|
|
checks: command.checks,
|
|
requestId: command.requestId,
|
|
lifetime,
|
|
});
|
|
} catch (error: any) {
|
|
if ("failedAttempts" in error && error.failedAttempts) {
|
|
const lockoutSettings = await getLockoutSettings({
|
|
serviceUrl,
|
|
orgId: command.organization,
|
|
});
|
|
|
|
const hasLimit =
|
|
lockoutSettings?.maxPasswordAttempts !== undefined && lockoutSettings?.maxPasswordAttempts > BigInt(0);
|
|
const locked = hasLimit && error.failedAttempts >= lockoutSettings?.maxPasswordAttempts;
|
|
const messageKey = hasLimit ? "errors.failedToAuthenticate" : "errors.failedToAuthenticateNoLimit";
|
|
|
|
return {
|
|
error: t(messageKey, {
|
|
failedAttempts: error.failedAttempts,
|
|
maxPasswordAttempts: hasLimit ? (lockoutSettings?.maxPasswordAttempts).toString() : "?",
|
|
lockoutMessage: locked ? t("errors.accountLockedContactAdmin") : "",
|
|
}),
|
|
};
|
|
}
|
|
throw error;
|
|
}
|
|
|
|
if (!session?.factors?.user?.id) {
|
|
return { error: t("errors.couldNotCreateSessionForUser") };
|
|
}
|
|
|
|
const userResponse = await getUserByID({
|
|
serviceUrl,
|
|
userId: session?.factors?.user?.id,
|
|
});
|
|
|
|
if (!userResponse.user) {
|
|
return { error: t("errors.userNotFound") };
|
|
}
|
|
|
|
user = userResponse.user;
|
|
}
|
|
|
|
if (!loginSettings) {
|
|
loginSettings = await getLoginSettings({
|
|
serviceUrl,
|
|
organization: command.organization ?? session.factors?.user?.organizationId,
|
|
});
|
|
}
|
|
|
|
if (!session?.factors?.user?.id || !sessionCookie) {
|
|
return { error: t("errors.couldNotCreateSessionForUser") };
|
|
}
|
|
|
|
const humanUser = user.type.case === "human" ? user.type.value : undefined;
|
|
|
|
const expirySettings = await getPasswordExpirySettings({
|
|
serviceUrl,
|
|
orgId: command.organization ?? session.factors?.user?.organizationId,
|
|
});
|
|
|
|
// check if the user has to change password first
|
|
const passwordChangedCheck = checkPasswordChangeRequired(
|
|
expirySettings,
|
|
session,
|
|
humanUser,
|
|
command.organization,
|
|
command.requestId,
|
|
);
|
|
|
|
if (passwordChangedCheck?.redirect) {
|
|
return passwordChangedCheck;
|
|
}
|
|
|
|
// throw error if user is in initial state here and do not continue
|
|
if (user.state === UserState.INITIAL) {
|
|
return { error: t("errors.initialUserNotSupported") };
|
|
}
|
|
|
|
// check to see if user was verified
|
|
const emailVerificationCheck = checkEmailVerification(session, humanUser, command.organization, command.requestId);
|
|
|
|
if (emailVerificationCheck?.redirect) {
|
|
return emailVerificationCheck;
|
|
}
|
|
|
|
// 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,
|
|
});
|
|
if (response.authMethodTypes && response.authMethodTypes.length) {
|
|
authMethods = response.authMethodTypes;
|
|
}
|
|
}
|
|
|
|
if (!authMethods) {
|
|
return { error: t("errors.couldNotVerifyPassword") };
|
|
}
|
|
|
|
const mfaFactorCheck = await checkMFAFactors(
|
|
serviceUrl,
|
|
session,
|
|
loginSettings,
|
|
authMethods,
|
|
command.organization,
|
|
command.requestId,
|
|
);
|
|
|
|
if (mfaFactorCheck?.redirect) {
|
|
return mfaFactorCheck;
|
|
}
|
|
|
|
if (command.requestId && session.id) {
|
|
// OIDC/SAML flow - use completeFlowOrGetUrl for proper handling
|
|
console.log("Password auth: OIDC/SAML flow with requestId:", command.requestId, "sessionId:", session.id);
|
|
const result = await completeFlowOrGetUrl(
|
|
{
|
|
sessionId: session.id,
|
|
requestId: command.requestId,
|
|
organization: command.organization ?? session.factors?.user?.organizationId,
|
|
},
|
|
loginSettings?.defaultRedirectUri,
|
|
);
|
|
console.log("Password auth: OIDC/SAML flow result:", result);
|
|
|
|
// Safety net - ensure we always return a valid object
|
|
if (!result || typeof result !== "object" || (!("redirect" in result) && !("error" in result))) {
|
|
console.error("Password auth: Invalid result from completeFlowOrGetUrl (OIDC/SAML):", result);
|
|
return { error: "Authentication completed but navigation failed" };
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Regular flow (no requestId) - return URL for client-side navigation
|
|
console.log("Password auth: Regular flow with loginName:", session.factors.user.loginName);
|
|
const result = await completeFlowOrGetUrl(
|
|
{
|
|
loginName: session.factors.user.loginName,
|
|
organization: session.factors?.user?.organizationId,
|
|
},
|
|
loginSettings?.defaultRedirectUri,
|
|
);
|
|
console.log("Password auth: Regular flow result:", result);
|
|
|
|
// Safety net - ensure we always return a valid object
|
|
if (!result || typeof result !== "object" || (!("redirect" in result) && !("error" in result))) {
|
|
console.error("Password auth: Invalid result from completeFlowOrGetUrl:", result);
|
|
return { error: "Authentication completed but navigation failed" };
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// 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 t = await getTranslations("password");
|
|
|
|
// check for init state
|
|
const { user } = await getUserByID({
|
|
serviceUrl,
|
|
userId: command.userId,
|
|
});
|
|
|
|
if (!user || user.userId !== command.userId) {
|
|
return { error: t("errors.couldNotSendResetLink") };
|
|
}
|
|
const userId = user.userId;
|
|
|
|
if (user.state === UserState.INITIAL) {
|
|
return { error: t("errors.userInitialStateNotSupported") };
|
|
}
|
|
|
|
// check if the user has no password set in order to set a password
|
|
if (!command.code) {
|
|
const authmethods = await listAuthenticationMethodTypes({
|
|
serviceUrl,
|
|
userId,
|
|
});
|
|
|
|
// if the user has no authmethods set, we need to check if the user was verified
|
|
if (authmethods.authMethodTypes.length !== 0) {
|
|
return {
|
|
error: t("errors.codeOrVerificationRequired"),
|
|
};
|
|
}
|
|
|
|
// check if a verification was done earlier
|
|
const hasValidUserVerificationCheck = await checkUserVerification(user.userId);
|
|
|
|
if (!hasValidUserVerificationCheck) {
|
|
return { error: t("errors.verificationRequired") };
|
|
}
|
|
}
|
|
|
|
return setUserPassword({
|
|
serviceUrl,
|
|
userId,
|
|
password: command.password,
|
|
code: command.code,
|
|
});
|
|
}
|
|
|
|
type CheckSessionAndSetPasswordCommand = {
|
|
sessionId: string;
|
|
password: string;
|
|
};
|
|
|
|
export async function checkSessionAndSetPassword({ sessionId, password }: CheckSessionAndSetPasswordCommand) {
|
|
const _headers = await headers();
|
|
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
|
const t = await getTranslations("password");
|
|
|
|
let sessionCookie;
|
|
try {
|
|
sessionCookie = await getSessionCookieById({ sessionId });
|
|
} catch (error) {
|
|
console.error("Error getting session cookie:", error);
|
|
return { error: "Could not load session cookie" };
|
|
}
|
|
|
|
let session;
|
|
try {
|
|
const sessionResponse = await getSession({
|
|
serviceUrl,
|
|
sessionId: sessionCookie.id,
|
|
sessionToken: sessionCookie.token,
|
|
});
|
|
session = sessionResponse.session;
|
|
} catch (error) {
|
|
console.error("Error getting session:", error);
|
|
return { error: "Could not load session" };
|
|
}
|
|
|
|
if (!session || !session.factors?.user?.id) {
|
|
return { error: t("errors.couldNotLoadSession") };
|
|
}
|
|
|
|
const payload = create(SetPasswordRequestSchema, {
|
|
userId: session.factors.user.id,
|
|
newPassword: {
|
|
password,
|
|
},
|
|
});
|
|
|
|
// 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,
|
|
});
|
|
} catch (error) {
|
|
console.error("Error getting auth methods:", error);
|
|
return { error: "Could not load auth methods" };
|
|
}
|
|
|
|
if (!authmethods) {
|
|
return { error: t("errors.couldNotLoadAuthMethods") };
|
|
}
|
|
|
|
let loginSettings;
|
|
try {
|
|
loginSettings = await getLoginSettings({
|
|
serviceUrl,
|
|
organization: session.factors.user.organizationId,
|
|
});
|
|
} catch (error) {
|
|
console.error("Error getting login settings:", error);
|
|
return { error: "Could not load login settings" };
|
|
}
|
|
|
|
const forceMfa = !!(loginSettings?.forceMfa || loginSettings?.forceMfaLocalOnly);
|
|
|
|
// 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) => {
|
|
// throw error if failed precondition (ex. User is not yet initialized)
|
|
if (error.code === 9 && error.message) {
|
|
return { error: t("errors.failedPrecondition") };
|
|
}
|
|
return { error: "Could not set password" };
|
|
});
|
|
} else {
|
|
const transport = async (serviceUrl: string, token: string) => {
|
|
return createServerTransport(token, serviceUrl);
|
|
};
|
|
|
|
const myUserService = async (serviceUrl: string, sessionToken: string) => {
|
|
const transportPromise = await transport(serviceUrl, sessionToken);
|
|
return createUserServiceClient(transportPromise);
|
|
};
|
|
|
|
const selfService = await myUserService(serviceUrl, `${sessionCookie.token}`);
|
|
|
|
return selfService
|
|
.setPassword(
|
|
{
|
|
userId: session.factors.user.id,
|
|
newPassword: { password, changeRequired: false },
|
|
},
|
|
{},
|
|
)
|
|
.catch((error: ConnectError) => {
|
|
console.log(error);
|
|
if (error.code === 7) {
|
|
return { error: t("errors.sessionNotValid") };
|
|
}
|
|
return { error: "Could not set the password" };
|
|
});
|
|
}
|
|
}
|