resetAuthPasswordWithToken function

Future<AuthPasswordResetResult> resetAuthPasswordWithToken({
  1. required AuthStore store,
  2. required PasswordHasher passwordHasher,
  3. required String token,
  4. required String newPassword,
  5. AuthTwoFactorTrustedDeviceStore? trustedDeviceStore,
  6. PasswordPolicy passwordPolicy = const PasswordPolicy(),
  7. DateTime? now,
})

Consumes a reset token, replaces the user's password, and revokes sessions.

Password policy validation runs before token consumption so a caller can correct weak input without burning a valid reset link. Once a valid token is consumed, failures are deliberately fail-closed: the token cannot be replayed to retry a potentially partially completed reset.

Implementation

Future<AuthPasswordResetResult> resetAuthPasswordWithToken({
  required AuthStore store,
  required PasswordHasher passwordHasher,
  required String token,
  required String newPassword,
  AuthTwoFactorTrustedDeviceStore? trustedDeviceStore,
  PasswordPolicy passwordPolicy = const PasswordPolicy(),
  DateTime? now,
}) async {
  final passwordError = passwordPolicy.validateRegistration(newPassword);
  if (passwordError != null) {
    throw AuthFlowException(passwordError);
  }

  final consumed = await consumeAuthPasswordResetToken(
    store: store.passwordResetTokens,
    token: token,
  );
  if (consumed == null) {
    throw AuthFlowException('invalid_password_reset_token');
  }

  final user = await Future.sync(() => store.users.findById(consumed.userId));
  if (user == null || user.id.trim().isEmpty) {
    throw AuthFlowException('invalid_password_reset_token');
  }

  final changedAt = (now ?? DateTime.now()).toUtc();
  await trustedDeviceStore?.revokeAll(user.id, now: changedAt);

  // Fail closed before changing credentials: invalidate JWTs and revoke every
  // server-side session first. If either operation fails, the password remains
  // unchanged. Durable adapters may additionally wrap these mutations and the
  // credential replacement in their own transaction.
  await Future.sync(() => store.jwtVersions.rotate(user.id));
  final sessionsRevoked = await Future.sync(
    () => store.sessions.revokeAllForUser(user.id, revokedAt: changedAt),
  );

  final passwordHash = passwordHasher.hash(newPassword);
  if (passwordHash.trim().isEmpty) {
    throw AuthFlowException('password_reset_failed');
  }
  final credentialsUpdated = await Future.sync(
    () => store.credentials.updatePasswordForUser(
      userId: user.id,
      passwordHash: passwordHash,
      updatedAt: changedAt,
    ),
  );
  if (credentialsUpdated <= 0) {
    throw AuthFlowException('password_reset_failed');
  }

  await Future.sync(() => store.passwordResetTokens.deleteForUser(user.id));
  return AuthPasswordResetResult(
    user: user,
    credentialsUpdated: credentialsUpdated,
    sessionsRevoked: sessionsRevoked,
  );
}