From 1ffb9968158a0d8db2a51fefb091f3042cda559c Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Tue, 20 May 2025 09:52:59 +0200 Subject: [PATCH] improve password handling --- .../login/src/components/register-passkey.tsx | 11 ++ apps/login/src/lib/server/passkeys.ts | 44 +++++- apps/login/src/lib/server/password.ts | 135 +++++++++--------- apps/login/src/lib/zitadel.ts | 47 +----- 4 files changed, 124 insertions(+), 113 deletions(-) diff --git a/apps/login/src/components/register-passkey.tsx b/apps/login/src/components/register-passkey.tsx index 163ab507b8..8687312bbc 100644 --- a/apps/login/src/components/register-passkey.tsx +++ b/apps/login/src/components/register-passkey.tsx @@ -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", diff --git a/apps/login/src/lib/server/passkeys.ts b/apps/login/src/lib/server/passkeys.ts index 73d12043b0..c5ad990c61 100644 --- a/apps/login/src/lib/server/passkeys.ts +++ b/apps/login/src/lib/server/passkeys.ts @@ -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 { +): Promise { 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) { diff --git a/apps/login/src/lib/server/password.ts b/apps/login/src/lib/server/password.ts index 9e256c71d9..26adb56c7b 100644 --- a/apps/login/src/lib/server/password.ts +++ b/apps/login/src/lib/server/password.ts @@ -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,67 +404,32 @@ 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; - } + const transport = async (serviceUrl: string, token: string) => { + return createServerTransport(token, { + baseUrl: serviceUrl, }); - } else { - const transport = async (serviceUrl: string, token: string) => { - return createServerTransport(token, { - baseUrl: serviceUrl, - }); - }; + }; - const myUserService = async (serviceUrl: string, sessionToken: string) => { - const transportPromise = await transport(serviceUrl, sessionToken); - return createUserServiceClient(transportPromise); - }; + const myUserService = async (serviceUrl: string, sessionToken: string) => { + const transportPromise = await transport(serviceUrl, sessionToken); + return createUserServiceClient(transportPromise); + }; - const selfService = await myUserService( - serviceUrl, - `${sessionCookie.token}`, - ); + 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: "Session is not valid." }; - } - throw error; - }); - } + return selfService + .setPassword( + { + userId: session.factors.user.id, + newPassword: { password, changeRequired: false }, + }, + {}, + ) + .catch((error: ConnectError) => { + console.log(error); + if (error.code === 7) { + return { error: "Session is not valid." }; + } + throw error; + }); } diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts index 409e63e6cf..d1fe83434d 100644 --- a/apps/login/src/lib/zitadel.ts +++ b/apps/login/src/lib/zitadel.ts @@ -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,