All files / src/security/userAndConnexion userPasswordService.ts

26.25% Statements 21/80
100% Branches 0/0
0% Functions 0/2
26.25% Lines 21/80

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 801x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x                                                                                                               1x 1x 1x 1x 1x        
 
import bcrypt from 'bcrypt'
import { db } from '../../db'
import { ModelTypes } from '../../cache/dbs/index.generated'
 
import { lockUserAndThrow } from './userLockService'
 
import { getId, timeout, random } from 'topkat-utils'
import { getPluginConfig } from '../../plugins/pluginSystem'
 
 
 
const tooMuchPasswordMessages = 'tooMuchPasswordAttempts'
 
 
export async function comparePasswordAddAttemptAndLockIfNecessary(
  ctx: Ctx,
  password: string,
  hash: string,
  userOrId: ModelTypes['user'] | string,
  conf: {
    loginRetrialCountResetTimeMinutes?: number
    maxPasswordRetry?: number
    throwIfError?: boolean
  } = {}
): Promise<boolean> {

  const loginRetrialCountResetTimeMs = (conf.loginRetrialCountResetTimeMinutes || 30) * 60 * 1000

  await timeout(random(1, 80)) // anti timer attack

  const user = typeof userOrId === 'string' ? await db.user.getById(ctx, userOrId, { triggerErrorIfNotSet: true }) : userOrId

  const isValid = await bcrypt.compare(password, hash)

  if (isValid) {
    await db.user.update(ctx.GM, getId(user), {
      lastPasswordCompareTime: null,
      passwordRetrialNb: 0,
      ...(user.lockedReason?.includes(tooMuchPasswordMessages) ? {
        isLocked: false,
        lockUntil: null,
      } : {}),
    })
    return true
  } else {
    if ((new Date(user.lastPasswordCompareTime)).getTime() < Date.now() - loginRetrialCountResetTimeMs) {
      // THIS IS THE FIRST ATTEMPT IN A LONG TIME so we reset password attempts
      await db.user.update(ctx.GM, getId(user), {
        lastPasswordCompareTime: new Date(),
        passwordRetrialNb: 1,
      })
    } else {
      // MULTIPLE ATTEMPTS, increment attemps number and lock user is needed
      if (user.passwordRetrialNb >= (conf.maxPasswordRetry || 4)) {
        // TOO MUCH ATTEMPTS
        await lockUserAndThrow(ctx, getId(user), tooMuchPasswordMessages)
      } else {
        // ACCEPTABLE NB OF ATTEMPTS
        await db.user.update(ctx.GM, getId(user), {
          lastPasswordCompareTime: new Date(),
          $inc: { passwordRetrialNb: 1 }
        })
      }
    }

    if (typeof conf.throwIfError === 'undefined' || conf.throwIfError === true) throw ctx.error.wrongPassword({ passwordRetrialNb: user.passwordRetrialNb })

    return false
  }
}
 
 
 
 
export async function encryptPassword(password: string): Promise<string> {
  const { saltRoundsForPasswordEncryption } = getPluginConfig('GDmanagedLogin')
  const salt = bcrypt.genSaltSync(saltRoundsForPasswordEncryption || 11)
  return bcrypt.hashSync(password, salt)
}