From 63656e16fbf9cc8536469e59599c5472661891b4 Mon Sep 17 00:00:00 2001 From: Max Peintner Date: Mon, 27 Jan 2025 14:15:05 +0100 Subject: [PATCH] handle password attempts error --- apps/login/src/lib/server/cookie.ts | 128 +++++++++++++++----------- apps/login/src/lib/server/password.ts | 51 +++++++--- apps/login/src/lib/zitadel.ts | 8 ++ packages/zitadel-proto/package.json | 2 +- 4 files changed, 123 insertions(+), 66 deletions(-) diff --git a/apps/login/src/lib/server/cookie.ts b/apps/login/src/lib/server/cookie.ts index 91447174f67..795a41cd8d6 100644 --- a/apps/login/src/lib/server/cookie.ts +++ b/apps/login/src/lib/server/cookie.ts @@ -8,6 +8,10 @@ import { setSession, } from "@/lib/zitadel"; import { Duration, timestampMs } from "@zitadel/client"; +import { + CredentialsCheckError, + ErrorDetail, +} from "@zitadel/proto/zitadel/message_pb"; import { Challenges, RequestChallenges, @@ -89,7 +93,16 @@ export async function createSessionForIdpAndUpdateCookie( userId, idpIntent, lifetime, - ); + ).catch((error: ErrorDetail | CredentialsCheckError) => { + console.error("Could not set session", error); + if ("failedAttempts" in error && error.failedAttempts) { + throw { + error: `Failed to authenticate: You had ${error.failedAttempts} password attempts.`, + failedAttempts: error.failedAttempts, + }; + } + throw error; + }); if (!createdSession) { throw "Could not create session"; @@ -148,57 +161,68 @@ export async function setSessionAndUpdateCookie( challenges, checks, lifetime, - ).then((updatedSession) => { - if (updatedSession) { - const sessionCookie: CustomCookieData = { - id: recentCookie.id, - token: updatedSession.sessionToken, - creationTs: recentCookie.creationTs, - expirationTs: recentCookie.expirationTs, - // just overwrite the changeDate with the new one - changeTs: updatedSession.details?.changeDate - ? `${timestampMs(updatedSession.details.changeDate)}` - : "", - loginName: recentCookie.loginName, - organization: recentCookie.organization, - }; + ) + .then((updatedSession) => { + if (updatedSession) { + const sessionCookie: CustomCookieData = { + id: recentCookie.id, + token: updatedSession.sessionToken, + creationTs: recentCookie.creationTs, + expirationTs: recentCookie.expirationTs, + // just overwrite the changeDate with the new one + changeTs: updatedSession.details?.changeDate + ? `${timestampMs(updatedSession.details.changeDate)}` + : "", + loginName: recentCookie.loginName, + organization: recentCookie.organization, + }; - if (authRequestId) { - sessionCookie.authRequestId = authRequestId; - } - - return getSession({ - sessionId: sessionCookie.id, - sessionToken: sessionCookie.token, - }).then((response) => { - if (response?.session && response.session.factors?.user?.loginName) { - const { session } = response; - const newCookie: CustomCookieData = { - id: sessionCookie.id, - token: updatedSession.sessionToken, - creationTs: sessionCookie.creationTs, - expirationTs: sessionCookie.expirationTs, - // just overwrite the changeDate with the new one - changeTs: updatedSession.details?.changeDate - ? `${timestampMs(updatedSession.details.changeDate)}` - : "", - loginName: session.factors?.user?.loginName ?? "", - organization: session.factors?.user?.organizationId ?? "", - }; - - if (sessionCookie.authRequestId) { - newCookie.authRequestId = sessionCookie.authRequestId; - } - - return updateSessionCookie(sessionCookie.id, newCookie).then(() => { - return { challenges: updatedSession.challenges, ...session }; - }); - } else { - throw "could not get session or session does not have loginName"; + if (authRequestId) { + sessionCookie.authRequestId = authRequestId; } - }); - } else { - throw "Session not be set"; - } - }); + + return getSession({ + sessionId: sessionCookie.id, + sessionToken: sessionCookie.token, + }).then((response) => { + if (response?.session && response.session.factors?.user?.loginName) { + const { session } = response; + const newCookie: CustomCookieData = { + id: sessionCookie.id, + token: updatedSession.sessionToken, + creationTs: sessionCookie.creationTs, + expirationTs: sessionCookie.expirationTs, + // just overwrite the changeDate with the new one + changeTs: updatedSession.details?.changeDate + ? `${timestampMs(updatedSession.details.changeDate)}` + : "", + loginName: session.factors?.user?.loginName ?? "", + organization: session.factors?.user?.organizationId ?? "", + }; + + if (sessionCookie.authRequestId) { + newCookie.authRequestId = sessionCookie.authRequestId; + } + + return updateSessionCookie(sessionCookie.id, newCookie).then(() => { + return { challenges: updatedSession.challenges, ...session }; + }); + } else { + throw "could not get session or session does not have loginName"; + } + }); + } else { + throw "Session not be set"; + } + }) + .catch((error: ErrorDetail | CredentialsCheckError) => { + console.error("Could not set session", error); + if ("failedAttempts" in error && error.failedAttempts) { + throw { + error: `Failed to authenticate: You had ${error.failedAttempts} password attempts.`, + failedAttempts: error.failedAttempts, + }; + } + throw error; + }); } diff --git a/apps/login/src/lib/server/password.ts b/apps/login/src/lib/server/password.ts index 6f1b79be62b..d69074aca3c 100644 --- a/apps/login/src/lib/server/password.ts +++ b/apps/login/src/lib/server/password.ts @@ -5,6 +5,7 @@ import { setSessionAndUpdateCookie, } from "@/lib/server/cookie"; import { + getLockoutSettings, getLoginSettings, getPasswordExpirySettings, getSession, @@ -98,24 +99,48 @@ export async function sendPassword(command: UpdateSessionCommand) { loginSettings = await getLoginSettings(command.organization); - session = await createSessionAndUpdateCookie( - checks, - undefined, - command.authRequestId, - loginSettings?.passwordCheckLifetime, - ); + 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" }; + } } // this is a fake error message to hide that the user does not even exist return { error: "Could not verify password" }; } else { - session = await setSessionAndUpdateCookie( - sessionCookie, - command.checks, - undefined, - command.authRequestId, - loginSettings?.passwordCheckLifetime, - ); + 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" }; diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts index 589b5a43a08..c7aa7c262c9 100644 --- a/apps/login/src/lib/zitadel.ts +++ b/apps/login/src/lib/zitadel.ts @@ -81,6 +81,14 @@ export async function getLoginSettings(orgId?: string) { return useCache ? cacheWrapper(callback) : callback; } +export async function getLockoutSettings(orgId?: string) { + const callback = settingsService + .getLockoutSettings({ ctx: makeReqCtx(orgId) }, {}) + .then((resp) => (resp.settings ? resp.settings : undefined)); + + return useCache ? cacheWrapper(callback) : callback; +} + export async function getPasswordExpirySettings(orgId?: string) { const callback = settingsService .getPasswordExpirySettings({ ctx: makeReqCtx(orgId) }, {}) diff --git a/packages/zitadel-proto/package.json b/packages/zitadel-proto/package.json index 30c0932934f..abd06bb05db 100644 --- a/packages/zitadel-proto/package.json +++ b/packages/zitadel-proto/package.json @@ -14,7 +14,7 @@ ], "sideEffects": false, "scripts": { - "generate": "buf generate https://github.com/zitadel/zitadel.git --path ./proto/zitadel", + "generate": "buf generate https://github.com/zitadel/zitadel.git#branch=fix/9198-user_password_lockout_error_response --path ./proto/zitadel", "clean": "rm -rf zitadel .turbo node_modules google protoc-gen-openapiv2 validate" }, "dependencies": {