improve password handling

This commit is contained in:
Max Peintner
2025-05-20 09:52:59 +02:00
parent 6b9b52293d
commit 1ffb996815
4 changed files with 124 additions and 113 deletions

View File

@@ -83,6 +83,16 @@ export function RegisterPasskey({
return;
}
if ("error" in resp && resp.error) {
setError(resp.error);
return;
}
if (!("passkeyId" in resp)) {
setError("An error on registering passkey");
return;
}
const passkeyId = resp.passkeyId;
const options: CredentialCreationOptions =
(resp.publicKeyCredentialCreationOptions as CredentialCreationOptions) ??
@@ -92,6 +102,7 @@ export function RegisterPasskey({
setError("An error on registering passkey");
return;
}
options.publicKey.challenge = coerceToArrayBuffer(
options.publicKey.challenge,
"challenge",

View File

@@ -5,6 +5,7 @@ import {
getLoginSettings,
getSession,
getUserByID,
listAuthenticationMethodTypes,
registerPasskey,
verifyPasskeyRegistration as zitadelVerifyPasskeyRegistration,
} from "@/lib/zitadel";
@@ -14,7 +15,8 @@ import {
RegisterPasskeyResponse,
VerifyPasskeyRegistrationRequestSchema,
} from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { headers } from "next/headers";
import crypto from "crypto";
import { cookies, headers } from "next/headers";
import { userAgent } from "next/server";
import { getNextUrl } from "../client";
import {
@@ -22,6 +24,7 @@ import {
getSessionCookieById,
getSessionCookieByLoginName,
} from "../cookies";
import { getFingerprintId } from "../fingerprint";
import { getServiceUrlFromHeaders } from "../service-url";
import { checkEmailVerification } from "../verify-helper";
import { setSessionAndUpdateCookie } from "./cookie";
@@ -39,7 +42,7 @@ type RegisterPasskeyCommand = {
export async function registerPasskeyLink(
command: RegisterPasskeyCommand,
): Promise<RegisterPasskeyResponse> {
): Promise<RegisterPasskeyResponse | { error: string }> {
const { sessionId } = command;
const _headers = await headers();
@@ -57,6 +60,43 @@ export async function registerPasskeyLink(
sessionToken: sessionCookie.token,
});
if (!session?.session?.factors?.user?.id) {
return { error: "Could not determine user from session" };
}
const authmethods = await listAuthenticationMethodTypes({
serviceUrl,
userId: session?.session?.factors?.user?.id,
});
// if the user has no authmethods set, we need to check if the user was verified
// users are redirected from /authenticator/set to /password/set
if (authmethods.authMethodTypes.length !== 0) {
return {
error:
"You have to provide a code or have a valid User Verification Check",
};
}
// check if a verification was done earlier
const cookiesList = await cookies();
const userAgentId = await getFingerprintId();
const verificationCheck = crypto
.createHash("sha256")
.update(`${user.userId}:${userAgentId}`)
.digest("hex");
const cookieValue = await cookiesList.get("verificationCheck")?.value;
if (!cookieValue) {
return { error: "User Verification Check has to be done" };
}
if (cookieValue !== verificationCheck) {
return { error: "User Verification Check has to be done" };
}
const [hostname, port] = host.split(":");
if (!hostname) {

View File

@@ -13,7 +13,6 @@ import {
listAuthenticationMethodTypes,
listUsers,
passwordReset,
setPassword,
setUserPassword,
} from "@/lib/zitadel";
import { ConnectError, create } from "@zitadel/client";
@@ -25,13 +24,12 @@ import {
} 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 {
AuthenticationMethodType,
SetPasswordRequestSchema,
} from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { headers } from "next/headers";
import { SetPasswordRequestSchema } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import crypto from "crypto";
import { cookies, headers } from "next/headers";
import { getNextUrl } from "../client";
import { getSessionCookieById, getSessionCookieByLoginName } from "../cookies";
import { getFingerprintId } from "../fingerprint";
import { getServiceUrlFromHeaders } from "../service-url";
import {
checkEmailVerification,
@@ -297,6 +295,7 @@ export async function sendPassword(command: UpdateSessionCommand) {
return { redirect: url };
}
// 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;
@@ -316,11 +315,50 @@ export async function changePassword(command: {
}
const userId = user.userId;
if (user.state === UserState.INITIAL) {
return { error: "User Initial State is not supported" };
}
// 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
// users are redirected from /authenticator/set to /password/set
if (authmethods.authMethodTypes.length !== 0) {
return {
error:
"You have to provide a code or have a valid User Verification Check",
};
}
// check if a verification was done earlier
const cookiesList = await cookies();
const userAgentId = await getFingerprintId();
const verificationCheck = crypto
.createHash("sha256")
.update(`${user.userId}:${userAgentId}`)
.digest("hex");
const cookieValue = await cookiesList.get("verificationCheck")?.value;
if (!cookieValue) {
return { error: "User Verification Check has to be done" };
}
if (cookieValue !== verificationCheck) {
return { error: "User Verification Check has to be done" };
}
}
return setUserPassword({
serviceUrl,
userId,
password: command.password,
user,
code: command.code,
});
}
@@ -366,37 +404,6 @@ export async function checkSessionAndSetPassword({
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({
serviceUrl,
organization: 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({ serviceUrl, 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 transport = async (serviceUrl: string, token: string) => {
return createServerTransport(token, {
baseUrl: serviceUrl,
@@ -408,10 +415,7 @@ export async function checkSessionAndSetPassword({
return createUserServiceClient(transportPromise);
};
const selfService = await myUserService(
serviceUrl,
`${sessionCookie.token}`,
);
const selfService = await myUserService(serviceUrl, `${sessionCookie.token}`);
return selfService
.setPassword(
@@ -428,5 +432,4 @@ export async function checkSessionAndSetPassword({
}
throw error;
});
}
}

View File

@@ -29,11 +29,7 @@ import {
SearchQuery,
SearchQuerySchema,
} from "@zitadel/proto/zitadel/user/v2/query_pb";
import {
SendInviteCodeSchema,
User,
UserState,
} from "@zitadel/proto/zitadel/user/v2/user_pb";
import { SendInviteCodeSchema } from "@zitadel/proto/zitadel/user/v2/user_pb";
import {
AddHumanUserRequest,
ResendEmailCodeRequest,
@@ -45,10 +41,8 @@ import {
VerifyPasskeyRegistrationRequest,
VerifyU2FRegistrationRequest,
} from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import crypto from "crypto";
import { unstable_cacheLife as cacheLife } from "next/cache";
import { cookies } from "next/headers";
import { getFingerprintId, getUserAgent } from "./fingerprint";
import { getUserAgent } from "./fingerprint";
import { createServiceForHost } from "./service";
const useCache = process.env.DEBUG !== "true";
@@ -1172,13 +1166,11 @@ export async function setUserPassword({
serviceUrl,
userId,
password,
user,
code,
}: {
serviceUrl: string;
userId: string;
password: string;
user: User;
code?: string;
}) {
let payload = create(SetPasswordRequestSchema, {
@@ -1188,41 +1180,6 @@ export async function setUserPassword({
},
});
// check if the user has no password set in order to set a password
if (!code) {
const authmethods = await listAuthenticationMethodTypes({
serviceUrl,
userId,
});
// if the user has no authmethods set, we can set a password otherwise we need a code
if (
!(authmethods.authMethodTypes.length === 0) &&
user.state !== UserState.INITIAL
) {
// check if a verification was done earlier
const cookiesList = await cookies();
const userAgentId = await getFingerprintId();
const verificationCheck = crypto
.createHash("sha256")
.update(`${user.userId}:${userAgentId}`)
.digest("hex");
await cookiesList.set({
name: "verificationCheck",
value: verificationCheck,
httpOnly: true,
path: "/",
maxAge: 300, // 5 minutes
});
return { error: "Provide a code to set a password" };
}
}
if (code) {
payload = {
...payload,