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; 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 passkeyId = resp.passkeyId;
const options: CredentialCreationOptions = const options: CredentialCreationOptions =
(resp.publicKeyCredentialCreationOptions as CredentialCreationOptions) ?? (resp.publicKeyCredentialCreationOptions as CredentialCreationOptions) ??
@@ -92,6 +102,7 @@ export function RegisterPasskey({
setError("An error on registering passkey"); setError("An error on registering passkey");
return; return;
} }
options.publicKey.challenge = coerceToArrayBuffer( options.publicKey.challenge = coerceToArrayBuffer(
options.publicKey.challenge, options.publicKey.challenge,
"challenge", "challenge",

View File

@@ -5,6 +5,7 @@ import {
getLoginSettings, getLoginSettings,
getSession, getSession,
getUserByID, getUserByID,
listAuthenticationMethodTypes,
registerPasskey, registerPasskey,
verifyPasskeyRegistration as zitadelVerifyPasskeyRegistration, verifyPasskeyRegistration as zitadelVerifyPasskeyRegistration,
} from "@/lib/zitadel"; } from "@/lib/zitadel";
@@ -14,7 +15,8 @@ import {
RegisterPasskeyResponse, RegisterPasskeyResponse,
VerifyPasskeyRegistrationRequestSchema, VerifyPasskeyRegistrationRequestSchema,
} from "@zitadel/proto/zitadel/user/v2/user_service_pb"; } 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 { userAgent } from "next/server";
import { getNextUrl } from "../client"; import { getNextUrl } from "../client";
import { import {
@@ -22,6 +24,7 @@ import {
getSessionCookieById, getSessionCookieById,
getSessionCookieByLoginName, getSessionCookieByLoginName,
} from "../cookies"; } from "../cookies";
import { getFingerprintId } from "../fingerprint";
import { getServiceUrlFromHeaders } from "../service-url"; import { getServiceUrlFromHeaders } from "../service-url";
import { checkEmailVerification } from "../verify-helper"; import { checkEmailVerification } from "../verify-helper";
import { setSessionAndUpdateCookie } from "./cookie"; import { setSessionAndUpdateCookie } from "./cookie";
@@ -39,7 +42,7 @@ type RegisterPasskeyCommand = {
export async function registerPasskeyLink( export async function registerPasskeyLink(
command: RegisterPasskeyCommand, command: RegisterPasskeyCommand,
): Promise<RegisterPasskeyResponse> { ): Promise<RegisterPasskeyResponse | { error: string }> {
const { sessionId } = command; const { sessionId } = command;
const _headers = await headers(); const _headers = await headers();
@@ -57,6 +60,43 @@ export async function registerPasskeyLink(
sessionToken: sessionCookie.token, 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(":"); const [hostname, port] = host.split(":");
if (!hostname) { if (!hostname) {

View File

@@ -13,7 +13,6 @@ import {
listAuthenticationMethodTypes, listAuthenticationMethodTypes,
listUsers, listUsers,
passwordReset, passwordReset,
setPassword,
setUserPassword, setUserPassword,
} from "@/lib/zitadel"; } from "@/lib/zitadel";
import { ConnectError, create } from "@zitadel/client"; import { ConnectError, create } from "@zitadel/client";
@@ -25,13 +24,12 @@ import {
} from "@zitadel/proto/zitadel/session/v2/session_service_pb"; } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { User, UserState } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { User, UserState } from "@zitadel/proto/zitadel/user/v2/user_pb";
import { import { SetPasswordRequestSchema } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
AuthenticationMethodType, import crypto from "crypto";
SetPasswordRequestSchema, import { cookies, headers } from "next/headers";
} from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { headers } from "next/headers";
import { getNextUrl } from "../client"; import { getNextUrl } from "../client";
import { getSessionCookieById, getSessionCookieByLoginName } from "../cookies"; import { getSessionCookieById, getSessionCookieByLoginName } from "../cookies";
import { getFingerprintId } from "../fingerprint";
import { getServiceUrlFromHeaders } from "../service-url"; import { getServiceUrlFromHeaders } from "../service-url";
import { import {
checkEmailVerification, checkEmailVerification,
@@ -297,6 +295,7 @@ export async function sendPassword(command: UpdateSessionCommand) {
return { redirect: url }; return { redirect: url };
} }
// this function lets users with code set a password or users with valid User Verification Check
export async function changePassword(command: { export async function changePassword(command: {
code?: string; code?: string;
userId: string; userId: string;
@@ -316,11 +315,50 @@ export async function changePassword(command: {
} }
const userId = user.userId; 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({ return setUserPassword({
serviceUrl, serviceUrl,
userId, userId,
password: command.password, password: command.password,
user,
code: command.code, code: command.code,
}); });
} }
@@ -366,67 +404,32 @@ export async function checkSessionAndSetPassword({
return { error: "Could not load auth methods" }; return { error: "Could not load auth methods" };
} }
const requiredAuthMethodsForForceMFA = [ const transport = async (serviceUrl: string, token: string) => {
AuthenticationMethodType.OTP_EMAIL, return createServerTransport(token, {
AuthenticationMethodType.OTP_SMS, baseUrl: serviceUrl,
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,
});
};
const myUserService = async (serviceUrl: string, sessionToken: string) => { const myUserService = async (serviceUrl: string, sessionToken: string) => {
const transportPromise = await transport(serviceUrl, sessionToken); const transportPromise = await transport(serviceUrl, sessionToken);
return createUserServiceClient(transportPromise); return createUserServiceClient(transportPromise);
}; };
const selfService = await myUserService( const selfService = await myUserService(serviceUrl, `${sessionCookie.token}`);
serviceUrl,
`${sessionCookie.token}`,
);
return selfService return selfService
.setPassword( .setPassword(
{ {
userId: session.factors.user.id, userId: session.factors.user.id,
newPassword: { password, changeRequired: false }, newPassword: { password, changeRequired: false },
}, },
{}, {},
) )
.catch((error: ConnectError) => { .catch((error: ConnectError) => {
console.log(error); console.log(error);
if (error.code === 7) { if (error.code === 7) {
return { error: "Session is not valid." }; return { error: "Session is not valid." };
} }
throw error; throw error;
}); });
}
} }

View File

@@ -29,11 +29,7 @@ import {
SearchQuery, SearchQuery,
SearchQuerySchema, SearchQuerySchema,
} from "@zitadel/proto/zitadel/user/v2/query_pb"; } from "@zitadel/proto/zitadel/user/v2/query_pb";
import { import { SendInviteCodeSchema } from "@zitadel/proto/zitadel/user/v2/user_pb";
SendInviteCodeSchema,
User,
UserState,
} from "@zitadel/proto/zitadel/user/v2/user_pb";
import { import {
AddHumanUserRequest, AddHumanUserRequest,
ResendEmailCodeRequest, ResendEmailCodeRequest,
@@ -45,10 +41,8 @@ import {
VerifyPasskeyRegistrationRequest, VerifyPasskeyRegistrationRequest,
VerifyU2FRegistrationRequest, VerifyU2FRegistrationRequest,
} from "@zitadel/proto/zitadel/user/v2/user_service_pb"; } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import crypto from "crypto";
import { unstable_cacheLife as cacheLife } from "next/cache"; import { unstable_cacheLife as cacheLife } from "next/cache";
import { cookies } from "next/headers"; import { getUserAgent } from "./fingerprint";
import { getFingerprintId, getUserAgent } from "./fingerprint";
import { createServiceForHost } from "./service"; import { createServiceForHost } from "./service";
const useCache = process.env.DEBUG !== "true"; const useCache = process.env.DEBUG !== "true";
@@ -1172,13 +1166,11 @@ export async function setUserPassword({
serviceUrl, serviceUrl,
userId, userId,
password, password,
user,
code, code,
}: { }: {
serviceUrl: string; serviceUrl: string;
userId: string; userId: string;
password: string; password: string;
user: User;
code?: string; code?: string;
}) { }) {
let payload = create(SetPasswordRequestSchema, { 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) { if (code) {
payload = { payload = {
...payload, ...payload,