Authentication With Login and Password

Photo of Tomasz Nowacki

Tomasz Nowacki

Updated Jul 16, 2021 • 5 min read
lock

In this article you will learn how to properly authenticate users using login and password credentials and avoid common pitfalls

Common implementation #1


public async login(username, password): Promise {
  const user = await this.userRepository.findUserByUsername(username)

  if (!user) {
    throw new HttpError.NotFound('username not found') // #1
  }

  if (user.password !== this.cryptoProvider.hash(password)) { // #2
    throw new HttpError.Unauthorized('user password is invalid') // #3
  }

  return user
}

Issues with implementation #1

  • Potential user enumeration #1 and #3. The different errors returned allow an attacker to use different emails and observe the server response, checking which emails have accounts registered in our service.
  • #2. Comparing hashes as strings may allow a hostile actor to perform a timing attack. The attacker can try to obtain information about a password (e.g. its length) by observing the response time of a request. Most string comparison implementations are created to be highly performant, preventing further checking as soon as any difference in strings is detected. The details are highly dependent on the underlying string comparison implementation, but the early return mechanism exposes a threat in the form of a timing attack. To prevent this it's crucial to use secure password comparison functions - ones that compare passwords as a whole and don't rely on short circuits eg. binary comparison using the XOR operator.

Common implementation #2


public async login(username, password): Promise {
  const user = await this.userRepository.findUserByUsername(username)

  if (!user) {
    throw new HttpError.Unauthorized('username or password is invalid') // #1
  }

  if (user.password !== this.cryptoProvider.hash(password)) { // #2
    throw new HttpError.Unauthorized('user or password is invalid') // #3
  }

  return user
}

Issues with implementation #2

  • Potential user enumeration #1 and #3. A slightly better implementation than before due to the use of the same error to indicate that the authorization is invalid. Still, an attacker can try to perform the timing attack to check if a given email exists due to short-circuit logic - no password comparison if the user doesn't exist.
  • #2 same as in the previous example. Insecure password comparison.

Correct implementation


public async login(username, password): Promise {
  const user = await this.userRepository.findUserByUsername(username)
  const samePassword = await this.cryptoProvider
    .comparePassword(username.password, password) // #1

  if (user && samePassword) { // #2
    return user
  }

  throw new HttpError.Unauthorized('user password is invalid') // #3
}

Rationale

  • Same function flow for every case. Each call fetches the user from the DB and compares the password (no early returns #2). This way we eliminate the potential for the timing attacks.
  • Uniform response in case of an error #3. A single error indicating failure without disclosing the details.
  • The implementation is not presented here, but #1 indicates the use of secure password comparison function.

Examples of secure password comparison functions

References


Photo by Aubrey Odom on Unsplash

Photo of Tomasz Nowacki

More posts by this author

Tomasz Nowacki

How to build products fast?  We've just answered the question in our Digital Acceleration Editorial  Sign up to get access

We're Netguru!

At Netguru we specialize in designing, building, shipping and scaling beautiful, usable products with blazing-fast efficiency
Let's talk business!

Trusted by: