Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions packages/server/src/enterprise/services/account.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { IdentityManager } from '../../IdentityManager'
import { Platform, UserPlan } from '../../Interface'
import { GeneralErrorMessage } from '../../utils/constants'
import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
import logger from '../../utils/logger'
import { checkUsageLimit } from '../../utils/quotaUsage'
import { OrganizationUser, OrganizationUserStatus } from '../database/entities/organization-user.entity'
import { Organization, OrganizationName } from '../database/entities/organization.entity'
Expand All @@ -16,7 +17,7 @@ import { WorkspaceUser, WorkspaceUserStatus } from '../database/entities/workspa
import { Workspace, WorkspaceName } from '../database/entities/workspace.entity'
import { LoggedInUser, LoginActivityCode } from '../Interface.Enterprise'
import { destroyAllSessionsForUser } from '../middleware/passport/SessionPersistance'
import { compareHash } from '../utils/encryption.util'
import { compareHash, getHash, getPasswordSaltRounds, hashNeedsUpgrade } from '../utils/encryption.util'
import { sendPasswordResetEmail, sendVerificationEmailForCloud, sendWorkspaceAdd, sendWorkspaceInvite } from '../utils/sendEmail'
import { generateTempToken } from '../utils/tempTokenUtils'
import { validatePasswordOrThrow } from '../utils/validation.util'
Expand Down Expand Up @@ -469,6 +470,18 @@ export class AccountService {
await auditService.recordLoginActivity(user.email || '', LoginActivityCode.INCORRECT_CREDENTIAL, 'Login Failed')
throw new InternalFlowiseError(StatusCodes.UNAUTHORIZED, UserErrorMessage.INCORRECT_USER_EMAIL_OR_CREDENTIALS)
}

// If the stored hash was created with fewer salt rounds than the current minimum
// (e.g. 5 before we increased to 10), rehash with the current rounds on successful login.
if (hashNeedsUpgrade(user.credential!, getPasswordSaltRounds())) {
try {
const newHash = getHash(data.user.credential!)
await this.userService.saveUser({ ...user, credential: newHash }, queryRunner)
} catch (upgradeError) {
logger.warn(`Failed to upgrade password hash for user ${user.email}`, upgradeError)
}
}

if (user.status === UserStatus.UNVERIFIED) {
await auditService.recordLoginActivity(data.user.email || '', LoginActivityCode.REGISTRATION_PENDING, 'Login Failed')
throw new InternalFlowiseError(StatusCodes.UNAUTHORIZED, UserErrorMessage.USER_EMAIL_UNVERIFIED)
Expand Down Expand Up @@ -571,7 +584,8 @@ export class AccountService {

// all checks are done, now update the user password, don't forget to hash it and do not forget to clear the temp token
// leave the user status and other details as is
const salt = bcrypt.genSaltSync(parseInt(process.env.PASSWORD_SALT_HASH_ROUNDS || '5'))
const salt = bcrypt.genSaltSync(getPasswordSaltRounds())
// @ts-ignore
const hash = bcrypt.hashSync(password, salt)
data.user = user
data.user.credential = hash
Expand Down
29 changes: 28 additions & 1 deletion packages/server/src/enterprise/utils/encryption.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,35 @@ import bcrypt from 'bcryptjs'
import { AES, enc } from 'crypto-js'
import { getEncryptionKey } from '../../utils'

export function getPasswordSaltRounds(): number {
return parseInt(process.env.PASSWORD_SALT_HASH_ROUNDS || '10', 10)
}

/**
* Extracts the cost factor (salt rounds) from a bcrypt hash using bcrypt.getRounds().
* @returns The number of rounds used, or null if the string is not a valid bcrypt hash.
*/
export function getBcryptRoundsFromHash(hash: string): number | null {
try {
return bcrypt.getRounds(hash)
} catch {
return null
}
}

/**
* Checks if a stored bcrypt hash was created with fewer rounds than the current minimum,
* and should be rehashed for stronger security.
* @param storedHash The bcrypt hash stored in the database.
* @param minRounds The minimum acceptable number of salt rounds (e.g. 10).
*/
export function hashNeedsUpgrade(storedHash: string, minRounds: number): boolean {
const rounds = getBcryptRoundsFromHash(storedHash)
return rounds !== null && rounds < minRounds
}

export function getHash(value: string) {
const salt = bcrypt.genSaltSync(parseInt(process.env.PASSWORD_SALT_HASH_ROUNDS || '5'))
const salt = bcrypt.genSaltSync(getPasswordSaltRounds())
return bcrypt.hashSync(value, salt)
}

Expand Down
Loading