All files / src/plugins/secureAuth main.ts

47.27% Statements 52/110
0% Branches 0/1
0% Functions 0/1
47.27% Lines 52/110

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 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 1101x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x                                                                                                                    
import { AuthenticationMethod } from '../../ctx'
import bcrypt from 'bcrypt'
import { getId, timeout, random } from 'topkat-utils'
import { lockUserAndThrow } from '../../security/userAndConnexion/userLockService'
import { ModelTypes } from '../../cache/dbs/index.generated'
import { db } from '../../db'
import { AsMongooseBody } from '../../databases/mongo/types/mongoDbBaseTypes'
import { GDplugin } from '../GDplugin'
 
 
 
export type Name = 'GDdoubleAuthentication'
 
export const documentation = ``
 
 
const userFieldNbAttemps = {
  pincode: 'pinCodeRetrialNb',
  '2FA': '_2FAretrialNb',
  biometricAuthToken: 'biometricAuthRetrialNb'
} satisfies Record<AuthenticationMethod, keyof ModelTypes['user']>
 
const userFieldLastCompareTime = {
  pincode: 'lastPincodeCompareTime',
  '2FA': 'last2FACompareTime',
  biometricAuthToken: 'lastBiometricCompareTime'
} satisfies Record<AuthenticationMethod, keyof ModelTypes['user']>
 
 
 
 
export type PluginUserConfig = {
  enable: boolean,
  /** Secure connexion is Double authentication via sms, fingerprint or pinCode. This will configure nb attemps before locking for a configurable time period. Default: 3 */
  nbAttemptsForAuth?: Partial<Record<AuthenticationMethod, number>>
  /** Secure connexion is Double authentication via sms, fingerprint or pinCode. This will configure the time before unlocking after "nbAttemptsForAuth" fails. Default: 15 */
  resetTimeMinutesForSecureConnexion?: number
  /** Length of pincode. Default 4 */
  pinCodeLength?: number
}
 
 
export const defaultConfig: PluginUserConfig = {
  enable: true,
  nbAttemptsForAuth: { '2FA': 3, biometricAuthToken: 3, pincode: 3 },
  pinCodeLength: 4,
  resetTimeMinutesForSecureConnexion: 15,
}
 
 
 
export class GDdoubleAuthentication extends GDplugin<Name> {
  name = 'GDdoubleAuthentication' as const
  version = '1.0.0'

  config: PluginUserConfig

  constructor(config: PluginUserConfig) {
    super()
    this.config = { ...defaultConfig, ...config }
  }

  async compareAndAddAttempt(ctx: Ctx, type: AuthenticationMethod, token: string, userOrId: ModelTypes['user'] | string): Promise<void> {

    const timeBetweenTwoAttempts = this.config.resetTimeMinutesForSecureConnexion * 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 nbAttemptFieldNameInUser = userFieldNbAttemps[type]
    const lastCompareTimeFieldNameInUser = userFieldLastCompareTime[type]

    const lastCompareTime = (new Date(user[lastCompareTimeFieldNameInUser])).getTime()

    const allowedAttemptsNb = this.config.nbAttemptsForAuth?.[type] || 3

    // for a long period of time since last check, we reset the nbAttempts
    const userNbAttempts = lastCompareTime + timeBetweenTwoAttempts < Date.now() ? 0 : user[nbAttemptFieldNameInUser]

    if (userNbAttempts >= allowedAttemptsNb) {
      await lockUserAndThrow(ctx, getId(user), 'tooManyAttempsForSecureAuthentication', {
        errExtraInfos: { authType: type, allowedAttemptsNb, resetTime: Date.now() + timeBetweenTwoAttempts },
      })
    }

    let success = false
    const fieldsToUpdate: Partial<AsMongooseBody<ModelTypes['userWrite']>> = {}

    if (type === 'pincode' && user.pinCode && await bcrypt.compare(token, user.pinCode)) {
      success = true
    } else if (type === '2FA' && token === user._2FAcode) {
      success = true
      fieldsToUpdate._2FAcode = null
    } else if (type === 'biometricAuthToken' && token === user.biometricAuthToken) {
      success = true
    }

    // reset nb attempts or add one if wrong token
    Object.assign(fieldsToUpdate, success ? { [nbAttemptFieldNameInUser]: 0 } : { $inc: { [nbAttemptFieldNameInUser]: 1 } })

    await db.user.update(ctx.GM, getId(user), {
      ...fieldsToUpdate,
      [lastCompareTimeFieldNameInUser]: new Date(),
    })

    if (!success) throw ctx.error.wrongToken({ type })

  }
}