From 20d997bc5a6f1674d39de8daf7911ce7ce313250 Mon Sep 17 00:00:00 2001 From: "sonarly[bot]" <251243324+sonarly[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 17:52:53 +0100 Subject: [PATCH] Fix: Password validation requires data and hash arguments (#18189) ## Automated fix for [bug 5360](https://sonarly.com/issue/5360?type=bug) **Severity:** `critical` ### Summary When an existing user created via OAuth (with no password hash) attempts to sign in through the signInUp password flow, their null passwordHash is passed directly to bcrypt.compare, which throws an unhandled 'data and hash arguments required' error instead of a user-friendly message. ### User Impact Users who originally registered via Google or Microsoft OAuth and then attempt to sign in using email/password through the signInUp flow cannot authenticate. They see a server error instead of a helpful message like 'User was not created with email/password'. This blocks the user from accessing the CRM entirely. ### Root Cause Proximate cause: bcrypt.compare() at auth.util.ts:19 throws 'data and hash arguments required' because the passwordHash argument is null. 1. Why did bcrypt throw? Because compareHash() was called with a null passwordHash value. The compareHash function at auth.util.ts:19 passes its arguments directly to bcrypt.compare without any null check. 2. Why was passwordHash null? Because SignInUpService.validatePassword() at sign-in-up.service.ts:154 received a null passwordHash from AuthService.validatePassword() at auth.service.ts:232. The value userData.existingUser.passwordHash is null for users who were originally created via an OAuth provider (Google, Microsoft) and never set a password. 3. Why was there no null check before calling bcrypt? Because AuthService.validatePassword() at line 229-233 does not check whether userData.existingUser.passwordHash is null before passing it to signInUpService.validatePassword(). The TypeScript type annotation says passwordHash is string, but the UserEntity column is declared nullable: true (user.entity.ts:73), so the runtime value can be null despite the type system. 4. Why does this code path lack the null check when another path has it? The validateLoginWithPassword() method at auth.service.ts:177 correctly checks if (!user.passwordHash) and throws a user-friendly AuthException 'Incorrect login method'. This guard was added by Thomas Trompette on 2024-08-07 (commit 2abb6adb614). However, the signInUp flow's validatePassword() method was introduced later by Antoine Moreaux on 2025-03-17 (commit bda835b9f82) without replicating this same defensive check. 5. Why was this not caught? The signInUp validatePassword method was written assuming the existingUser would always have a passwordHash when the auth provider is Password. This assumption is incorrect because the signInUp flow can match an existing OAuth-created user (who has no passwordHash) when someone tries to sign up with email/password using an email already registered via OAuth. No test covers this cross-provider scenario in the signInUp path. Root cause: The AuthService.validatePassword() method (auth.service.ts:229-233) in the signInUp code path is missing a null guard on userData.existingUser.passwordHash before passing it to bcrypt.compare. The fix should check for null/undefined passwordHash and throw a descriptive AuthException, mirroring the existing guard in validateLoginWithPassword at line 177. **Introduced by:** Antoine Moreaux on 2025-03-17 in commit [`bda835b`](https://github.com/twentyhq/twenty/commit/bda835b9f82) ### Suggested Fix Added a null check on userData.existingUser.passwordHash inside the validatePassword private method of AuthService, immediately before calling signInUpService.validatePassword. When passwordHash is null or undefined (i.e. the user was created via OAuth and never set a password), an AuthException with code INVALID_INPUT is thrown with the user-friendly message 'User was not created with email/password'. This mirrors the identical guard that already exists in validateLoginWithPassword at line 177, preventing bcrypt.compare from receiving a null argument and crashing with an unhandled error. ### Evidence - **Code:** [packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts:229 - if (userData.type === 'existingUser') {](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts#L229) --- *Generated by [Sonarly](https://sonarly.com)* Co-authored-by: Sonarly Claude Code --- .../engine/core-modules/auth/services/auth.service.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts index b75ddf1d84..9db1345181 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts @@ -227,6 +227,15 @@ export class AuthService { } if (userData.type === 'existingUser') { + if (!userData.existingUser.passwordHash) { + throw new AuthException( + 'Incorrect login method', + AuthExceptionCode.INVALID_INPUT, + { + userFriendlyMessage: msg`User was not created with email/password`, + }, + ); + } await this.signInUpService.validatePassword({ password: authParams.password, passwordHash: userData.existingUser.passwordHash,