authenticate method
Returns the AuthUser's ID upon successful email/password verification.
Can throw the following EmailLoginServerException subclasses:
- EmailAccountNotFoundException if the email address is not registered in the database.
- EmailAuthenticationInvalidCredentialsException if the password is not valid for an existing account.
- EmailAuthenticationTooManyAttemptsException if the user has made too many failed attempts.
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;
}