Files
zitadel/apps/login/src/lib/server/password.ts

365 lines
9.6 KiB
TypeScript
Raw Normal View History

"use server";
import {
createSessionAndUpdateCookie,
setSessionAndUpdateCookie,
} from "@/lib/server/cookie";
2024-09-18 14:13:04 +02:00
import {
2025-01-27 14:15:05 +01:00
getLockoutSettings,
2024-11-21 14:12:13 +01:00
getLoginSettings,
2025-01-27 13:26:20 +01:00
getPasswordExpirySettings,
2024-12-12 18:13:49 +01:00
getSession,
2024-10-16 11:20:23 +02:00
getUserByID,
2024-09-18 14:13:04 +02:00
listAuthenticationMethodTypes,
listUsers,
passwordReset,
2024-10-16 11:20:23 +02:00
setPassword,
2024-12-12 18:13:49 +01:00
setUserPassword,
2024-09-18 14:13:04 +02:00
} from "@/lib/zitadel";
import { create } from "@zitadel/client";
2025-01-02 14:54:51 +01:00
import { createServerTransport } from "@zitadel/client/node";
2024-12-12 18:13:49 +01:00
import { createUserServiceClient } from "@zitadel/client/v2";
2024-09-18 14:13:04 +02:00
import {
Checks,
ChecksSchema,
} from "@zitadel/proto/zitadel/session/v2/session_service_pb";
2024-11-27 11:02:34 +01:00
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { User, UserState } from "@zitadel/proto/zitadel/user/v2/user_pb";
2024-12-12 18:13:49 +01:00
import {
AuthenticationMethodType,
SetPasswordRequestSchema,
} from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { headers } from "next/headers";
2024-11-21 14:12:13 +01:00
import { getNextUrl } from "../client";
2024-12-12 18:13:49 +01:00
import { getSessionCookieById, getSessionCookieByLoginName } from "../cookies";
2024-12-20 08:44:59 +01:00
import {
checkEmailVerification,
checkMFAFactors,
checkPasswordChangeRequired,
} from "../verify-helper";
type ResetPasswordCommand = {
loginName: string;
organization?: string;
authRequestId?: string;
};
export async function resetPassword(command: ResetPasswordCommand) {
const host = (await headers()).get("host");
const users = await listUsers({
2024-09-11 09:27:04 +02:00
loginName: command.loginName,
2024-09-05 13:38:03 +02:00
organizationId: command.organization,
});
if (
!users.details ||
2024-09-10 13:54:09 +02:00
users.details.totalResult !== BigInt(1) ||
!users.result[0].userId
) {
2024-09-19 09:16:11 +02:00
return { error: "Could not send Password Reset Link" };
}
const userId = users.result[0].userId;
return passwordReset(userId, host, command.authRequestId);
}
2024-09-18 14:13:04 +02:00
export type UpdateSessionCommand = {
loginName: string;
organization?: string;
checks: Checks;
authRequestId?: string;
};
export async function sendPassword(command: UpdateSessionCommand) {
let sessionCookie = await getSessionCookieByLoginName({
loginName: command.loginName,
organization: command.organization,
2024-09-18 15:57:26 +02:00
}).catch((error) => {
console.warn("Ignored error:", error);
2024-09-18 14:13:04 +02:00
});
let session;
let user: User;
2024-11-27 11:02:34 +01:00
let loginSettings: LoginSettings | undefined;
2024-09-18 14:13:04 +02:00
if (!sessionCookie) {
const users = await listUsers({
loginName: command.loginName,
organizationId: command.organization,
});
if (users.details?.totalResult == BigInt(1) && users.result[0].userId) {
user = users.result[0];
2024-09-18 14:13:04 +02:00
const checks = create(ChecksSchema, {
user: { search: { case: "userId", value: users.result[0].userId } },
password: { password: command.checks.password?.password },
});
2024-11-27 11:02:34 +01:00
loginSettings = await getLoginSettings(command.organization);
2025-01-27 14:15:05 +01:00
try {
session = await createSessionAndUpdateCookie(
checks,
undefined,
command.authRequestId,
loginSettings?.passwordCheckLifetime,
);
} catch (error: any) {
if ("failedAttempts" in error && error.failedAttempts) {
const lockoutSettings = await getLockoutSettings(
command.organization,
);
return {
error: `Failed to authenticate: You had ${error.failedAttempts} of ${lockoutSettings?.maxPasswordAttempts} password attempts.`,
};
}
return { error: "Could not create session for user" };
}
2024-09-18 14:13:04 +02:00
}
// this is a fake error message to hide that the user does not even exist
return { error: "Could not verify password" };
2024-09-18 14:13:04 +02:00
} else {
2025-01-27 14:15:05 +01:00
try {
session = await setSessionAndUpdateCookie(
sessionCookie,
command.checks,
undefined,
command.authRequestId,
loginSettings?.passwordCheckLifetime,
);
} catch (error: any) {
if ("failedAttempts" in error && error.failedAttempts) {
const lockoutSettings = await getLockoutSettings(command.organization);
return {
error: `Failed to authenticate: You had ${error.failedAttempts} of ${lockoutSettings?.maxPasswordAttempts} password attempts.`,
};
}
throw error;
}
if (!session?.factors?.user?.id) {
return { error: "Could not create session for user" };
}
const userResponse = await getUserByID(session?.factors?.user?.id);
if (!userResponse.user) {
2025-01-03 11:33:14 +01:00
return { error: "User not found in the system" };
}
user = userResponse.user;
2024-09-18 15:57:26 +02:00
}
2024-09-18 14:13:04 +02:00
2024-11-27 11:02:34 +01:00
if (!loginSettings) {
loginSettings = await getLoginSettings(
command.organization ?? session.factors?.user?.organizationId,
);
}
2024-09-18 15:57:26 +02:00
if (!session?.factors?.user?.id || !sessionCookie) {
return { error: "Could not create session for user" };
}
2024-09-18 14:13:04 +02:00
const humanUser = user.type.case === "human" ? user.type.value : undefined;
2024-12-03 13:48:07 +01:00
2025-01-27 13:26:20 +01:00
const expirySettings = await getPasswordExpirySettings(
command.organization ?? session.factors?.user?.organizationId,
);
// check if the user has to change password first
2024-12-20 10:57:56 +01:00
const passwordChangedCheck = checkPasswordChangeRequired(
2025-01-27 13:26:20 +01:00
expirySettings,
2024-12-20 08:44:59 +01:00
session,
humanUser,
command.organization,
command.authRequestId,
);
2024-12-20 10:57:56 +01:00
if (passwordChangedCheck?.redirect) {
return passwordChangedCheck;
}
2024-12-17 15:57:42 +01:00
// throw error if user is in initial state here and do not continue
if (user.state === UserState.INITIAL) {
return { error: "Initial User not supported" };
}
2024-12-19 15:12:50 +01:00
// check to see if user was verified
2024-12-20 10:57:56 +01:00
const emailVerificationCheck = checkEmailVerification(
2024-12-19 15:12:50 +01:00
session,
humanUser,
command.organization,
command.authRequestId,
);
2024-12-20 10:57:56 +01:00
if (emailVerificationCheck?.redirect) {
return emailVerificationCheck;
}
2024-12-19 15:12:50 +01:00
// if password, check if user has MFA methods
let authMethods;
if (command.checks && command.checks.password && session.factors?.user?.id) {
const response = await listAuthenticationMethodTypes(
session.factors.user.id,
);
if (response.authMethodTypes && response.authMethodTypes.length) {
authMethods = response.authMethodTypes;
}
2024-12-19 15:12:50 +01:00
}
2024-12-19 15:12:50 +01:00
if (!authMethods) {
return { error: "Could not verify password!" };
}
2024-12-23 16:26:20 +01:00
const mfaFactorCheck = checkMFAFactors(
2024-12-17 15:57:42 +01:00
session,
loginSettings,
authMethods,
command.organization,
command.authRequestId,
);
2024-12-23 16:26:20 +01:00
if (mfaFactorCheck?.redirect) {
return mfaFactorCheck;
}
2024-12-17 15:57:42 +01:00
if (command.authRequestId && session.id) {
2024-11-21 14:12:13 +01:00
const nextUrl = await getNextUrl(
{
sessionId: session.id,
authRequestId: command.authRequestId,
organization:
command.organization ?? session.factors?.user?.organizationId,
},
loginSettings?.defaultRedirectUri,
);
2024-11-26 11:51:24 +01:00
return { redirect: nextUrl };
}
2024-11-21 14:12:13 +01:00
const url = await getNextUrl(
{
loginName: session.factors.user.loginName,
organization: session.factors?.user?.organizationId,
},
loginSettings?.defaultRedirectUri,
);
2024-11-26 11:51:24 +01:00
return { redirect: url };
2024-09-18 14:13:04 +02:00
}
2024-10-15 17:27:08 +02:00
export async function changePassword(command: {
code?: string;
2024-10-15 17:27:08 +02:00
userId: string;
password: string;
}) {
// check for init state
2024-10-16 11:20:23 +02:00
const { user } = await getUserByID(command.userId);
2024-10-15 17:27:08 +02:00
2024-10-16 11:20:23 +02:00
if (!user || user.userId !== command.userId) {
2024-10-15 17:27:08 +02:00
return { error: "Could not send Password Reset Link" };
}
2024-10-16 11:20:23 +02:00
const userId = user.userId;
2024-10-15 17:27:08 +02:00
2024-12-12 18:13:49 +01:00
return setUserPassword(userId, command.password, user, command.code);
}
type CheckSessionAndSetPasswordCommand = {
sessionId: string;
password: string;
};
export async function checkSessionAndSetPassword({
sessionId,
password,
}: CheckSessionAndSetPasswordCommand) {
const sessionCookie = await getSessionCookieById({ sessionId });
const { session } = await getSession({
sessionId: sessionCookie.id,
sessionToken: sessionCookie.token,
});
if (!session || !session.factors?.user?.id) {
return { error: "Could not load session" };
}
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
const authmethods = await listAuthenticationMethodTypes(
session.factors.user.id,
);
if (!authmethods) {
return { error: "Could not load auth methods" };
}
const requiredAuthMethodsForForceMFA = [
AuthenticationMethodType.OTP_EMAIL,
AuthenticationMethodType.OTP_SMS,
AuthenticationMethodType.TOTP,
AuthenticationMethodType.U2F,
];
const hasNoMFAMethods = requiredAuthMethodsForForceMFA.every(
(method) => !authmethods.authMethodTypes.includes(method),
);
const loginSettings = await getLoginSettings(
session.factors.user.organizationId,
);
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 && hasNoMFAMethods) {
return setPassword(payload).catch((error) => {
// throw error if failed precondition (ex. User is not yet initialized)
if (error.code === 9 && error.message) {
return { error: "Failed precondition" };
} else {
throw error;
}
});
} else {
const myUserService = (sessionToken: string) => {
return createUserServiceClient(
createServerTransport(sessionToken, {
baseUrl: process.env.ZITADEL_API_URL!,
}),
);
};
const selfService = await myUserService(`${sessionCookie.token}`);
return selfService
.setPassword(
{
userId: session.factors.user.id,
newPassword: { password, changeRequired: false },
},
{},
)
.catch((error) => {
console.log(error);
if (error.code === 7) {
return { error: "Session is not valid." };
}
throw error;
});
}
2024-10-15 17:27:08 +02:00
}