authenticate method

Future<UuidValue> authenticate(
  1. Session session, {
  2. required String email,
  3. required String password,
  4. required Transaction? transaction,
})

Returns the AuthUser's ID upon successful email/password verification.

Can throw the following EmailLoginServerException subclasses:

The attempt is logged to the database outside of the transaction and can not be rolled back. A successful authentication clears the log for email, so the count only ever accumulates over failures.

Implementation

Future<UuidValue> authenticate(
  final Session session, {
  required String email,
  required final String password,
  required final Transaction? transaction,
}) async {
  email = email.normalizedEmail;

  // Records the attempt and reads the count as one atomic step. Counting
  // first and recording the failure afterwards left the account lookup and
  // the Argon2 verification inside the window, so requests sent together all
  // read the same pre-attempt count and the budget bounded a batch rather
  // than a time window.
  if (await _rateLimitUtil.hasTooManyAttempts(session, nonce: email)) {
    throw EmailAuthenticationTooManyAttemptsException();
  }

  final account = await EmailAccount.db.findFirstRow(
    session,
    where: (final t) => t.email.equals(email),
    transaction: transaction,
  );

  if (account == null) {
    throw EmailAccountNotFoundException();
  }

  if (!await _hashUtil.validateHashFromString(
    secret: password,
    hashString: account.passwordHash,
  )) {
    throw EmailAuthenticationInvalidCredentialsException();
  }

  // The attempt had to be counted before the outcome was known. Clearing on
  // success keeps this a limit on *failed* logins, so someone who mistypes a
  // few times and then gets it right is not left locked out.
  await _rateLimitUtil.deleteAttempts(
    session,
    nonce: email,
    olderThan: Duration.zero,
  );

  return account.authUserId;
}