Long awaited Class review

Alright first let me make some statements about this thread.

  1. I realize there are frameworks which do this. I do not care. I’ve
    explained reasonings behind this before and you are wasting time
    telling me about frameworks.
  2. I never claim this code is perfect. This
    is a learning exercise for me.
  3. I’m looking at the general OOP-ness
    of this and seeing what principles this class breaks. Security
    comments are also appreciated

The login can be played with here
http://www.codefundamentals.com/cadeui/login
admin@codefundamentals.com // password
After attempt 5, you will be locked out for 15 minutes so I recommend on attempt 4, you correctly log in.

When users submit their informatin, it goes to process-login.php

<?php
session_start();
require_once($_SERVER["DOCUMENT_ROOT"]."/cadeui/system/includes/bootstrap.php");
if($_SERVER["REQUEST_METHOD"]=="POST")
{
  $email=filter_input(INPUT_POST,"email");
  $password=filter_input(INPUT_POST,"password");
  $remember=filter_input(INPUT_POST,"remember");
  $login=new UserServices($pdo);

  $isValidLogin=$login->checkCredentials($email,$password,$remember);
  $isAjax=!empty($_SERVER["HTTP_X_REQUESTED_WITH"]) && strtolower($_SERVER["HTTP_X_REQUESTED_WITH"])=="xmlhttprequest";

  if($isAjax)
  {
    $result=["result" => $isValidLogin];
    header("Content-Type: application/json");
    exit(json_encode($result));
  }
  if($isValidLogin[0])
    header("Location: http://www.codefundamentals.com/cadeui/dashboard");
  else
    header("Location: http://www.codefundamentals.com/cadeui/login?error=$isValidLogin[1]");
}
else
    header("Location: http://www.codefundamentals.com/cadeui/login");
?>

If Javascript is on, it sends the data to this page and will get the results and do actions accordingly. This login will work with/without JS on. Now, the actual class this code references is this

<?php
class UserServices
{
  private $pdo;

  public function __construct(PDO $pdo)
  {
    $this->pdo=$pdo;
  }
  public function checkCredentials($email,$pass,$remember)
  {
    if($this->delayLogin())
    {
      $username=filter_var($email,FILTER_SANITIZE_EMAIL);
      $password=filter_var($pass,FILTER_SANITIZE_STRING);
      $remember=filter_var($remember,FILTER_VALIDATE_BOOLEAN);

      $findUser=$this->pdo->prepare("SELECT * FROM Users WHERE username=:username");
      $findUser->execute(array(":username" => $username));

      if($findUser->rowCount()===0)
      {
        $error="username";
        return array(false,$error);
      }
      $userDetails=$findUser->fetch(PDO::FETCH_ASSOC);
      if(password_verify($password,$userDetails["password"]))
      {
        if($remember)
          $this->setCookie($userDetails);

        $this->setSessions($userDetails,$this->findIP());
        return array(true,"");
      }
      else
      {
        $error="password";
        return array(false,$error);
      }
    }
    else
    {
      $error="attempts";
      return array(false,$error);
    }
  }
  private function delayLogin()
  {
    $grabIPuser=$this->pdo->prepare("SELECT * FROM Attempted_Logins WHERE ip_address = :ip_address");
    $grabIPuser->execute(array(":ip_address" => $this->findIP()));
    $ipUserDetails=$grabIPuser->fetch(PDO::FETCH_ASSOC);
    if($grabIPuser->rowCount()===0)
    {
      $insertIPdetails=$this->pdo->prepare("INSERT INTO Attempted_Logins (ip_address,login_attempts,last_login_time) VALUES(:ip_address,:login_attempts,:last_login_time)");
      $insertIPdetails->execute(array(":ip_address" => $this->findIP(),":login_attempts" => 0,":last_login_time" => $this->getDateTime("now","Y-m-d H:i:s")));
      return true;
    }
    else
    {
      $dbLastAttempt=$ipUserDetails["last_login_time"];
      $lastTimeAttempt=$this->getDateTime($dbLastAttempt,false);

      $timeDiff=$lastTimeAttempt->diff($this->getDateTime("now",false));
      if($timeDiff->format("%s")>2)
      {
        if($this->maxAttempts($ipUserDetails,$timeDiff->format("%i")))
          return true;
        else
          return false;
      }
      else
        return false;
    }
  }
  private function maxAttempts($dbIpUser,$resetMinutes)
  {
    if($dbIpUser["login_attempts"]<5)
    {
      $updateAttempts=$this->pdo->prepare("UPDATE Attempted_Logins SET login_attempts=:login_attempts WHERE ip_address=:ip_address");
      $updateAttempts->execute(array(":ip_address" => $dbIpUser["ip_address"],":login_attempts" => $dbIpUser["login_attempts"]+1));
      return true;
    }
    else
    {
      if($resetMinutes>=15)
      {
        $updateAttempts=$this->pdo->prepare("UPDATE Attempted_Logins SET login_attempts=0,last_login_time=:last_login_time WHERE ip_address=:ip_address");
        $updateAttempts->execute(array(":ip_address" => $dbIpUser["ip_address"],":last_login_time" => $this->getDateTime("now","Y-m-d H:i:s")));
        return true;
      }
      else
      {
        $updateAttempts=$this->pdo->prepare("UPDATE Attempted_Logins SET login_attempts=:login_attempts WHERE ip_address=:ip_address");
        $updateAttempts->execute(array(":ip_address" => $dbIpUser["ip_address"],":login_attempts" => $dbIpUser["login_attempts"]+1));
        return false;
      }
    }
  }
  private function setCookie($userInfo)
  {
    setcookie("rememberMe",$userInfo["authkey"],time() + (86400 * 14),"/");
  }
  public function userCookies($loginCookie,$action)
  {
    $cookieUser=$this->pdo->prepare("SELECT * FROM Users WHERE authkey=:authkey");
    $cookieUser->execute(array(":authkey" => $loginCookie));

    if($cookieUser->rowCount()===0)
      return false;
    else
    {
      $cookieUserInfo=$cookieUser->fetch(PDO::FETCH_ASSOC);
      if($action==="compare")
        return $cookieUser["authkey"];
      else if($action==="set")
        $this->setSessions($cookieUser,$this->findIP());
      else
        return null;
    }
  }
  private function setSessions($userDBinfo,$thisIP)
  {
    $_SESSION["loggedin"]=true;
    $_SESSION["username"]=$userDBinfo["username"];
    $_SESSION["userID"]=$userDBinfo["id"];
    $_SESSION["firstname"]=$userDBinfo["firstname"];

    $resetAttempts=$this->pdo->prepare("UPDATE Attempted_Logins SET login_attempts=0,last_login_time=:last_login_time WHERE ip_address=:ip_address");
    $resetAttempts->execute(array(":ip_address" => $thisIP,":last_login_time" => $this->getDateTime("now","Y-m-d H:i:s")));
  }
  private function getDateTime($when,$format)
  {
    $instance=new DateTime($when);
    if($format)
      return $instance->format($format);
    else
      return $instance;
  }
  private function findIP()
  {
    return getenv("HTTP_CLIENT_IP")?:
      getenv("HTTP_X_FORWARDED_FOR")?:
      getenv("HTTP_X_FORWARDED")?:
      getenv("HTTP_FORWARDED_FOR")?:
      getenv("HTTP_FORWARDED")?:
      getenv("REMOTE_ADDR");
  }
}
?>

This doesn’t relate to OOP, but it’s a small change that will make your code a little simpler:

if($this->maxAttempts($ipUserDetails,$timeDiff->format("%i")))
      return true;
    else
      return false;

The result of your IF condition is what you’re interested in, so skip the whole IF statement and just return the result directly:

return $this->maxAttempts($ipUserDetails,$timeDiff->format("%i"));
1 Like

Heh - good spot. Not sure why I didn’t think of htat.

I also changed the findIP() method to directly return REMOTE_ADDR SERVER variable. I also added another method for logging out to set a new authKey to the users account.

Cmon people, rip me a new one! Critique me :frowning: . Critique this class!

Adding on a small change…

Since we’re doing that, it might be more clear to rename maxAttempts. Right now it sounds like it returns the number of max attempts, since the method is named as a noun. Knowing what it does, a better name might be hasExceededMaxAttempts. Now getting a true or false back makes sense.

2 Likes

Great critiques! Loving it. Keep em coming.

This is the updated class

<?php
class UserServices
{
  private $pdo;

  public function __construct(PDO $pdo)
  {
    $this->pdo=$pdo;
  }
  public function checkCredentials($email,$pass,$remember)
  {
    if($this->delayLogin())
    {
      $username=filter_var($email,FILTER_SANITIZE_EMAIL);
      $password=filter_var($pass,FILTER_SANITIZE_STRING);
      $remember=filter_var($remember,FILTER_VALIDATE_BOOLEAN);

      $findUser=$this->pdo->prepare("SELECT * FROM Users WHERE username=:username");
      $findUser->execute(array(":username" => $username));

      if($findUser->rowCount()===0)
      {
        $error="username";
        return array(false,$error);
      }
      $userDetails=$findUser->fetch(PDO::FETCH_ASSOC);
      if(password_verify($password,$userDetails["password"]))
      {
        if($remember)
          $this->setCookie($userDetails);

        $this->setSessions($userDetails,$this->findIP());
        return array(true,"");
      }
      else
      {
        $error="password";
        return array(false,$error);
      }
    }
    else
    {
      $error="attempts";
      return array(false,$error);
    }
  }
  private function delayLogin()
  {
    $grabIPuser=$this->pdo->prepare("SELECT * FROM Attempted_Logins WHERE ip_address = :ip_address");
    $grabIPuser->execute(array(":ip_address" => $this->findIP()));
    if($grabIPuser->rowCount()===0)
    {
      $insertIPdetails=$this->pdo->prepare("INSERT INTO Attempted_Logins (ip_address,login_attempts,last_login_time) VALUES(:ip_address,:login_attempts,:last_login_time)");
      $insertIPdetails->execute(array(":ip_address" => $this->findIP(),":login_attempts" => 0,":last_login_time" => $this->getDateTime("now","Y-m-d H:i:s")));
      return true;
    }
    else
    {
      $ipUserDetails=$grabIPuser->fetch(PDO::FETCH_ASSOC);
      $lastTimeAttempt=$this->getDateTime($ipUserDetails["last_login_time"],false);

      $timeDiff=$lastTimeAttempt->diff($this->getDateTime("now",false));
      if($timeDiff->format("%s")>2)
        return $this->hasExceededMaxAttempts($ipUserDetails,$timeDiff->format("%i"));
      else
        return false;
    }
  }
  private function hasExceededMaxAttempts($dbIpUser,$resetMinutes)
  {
    if($dbIpUser["login_attempts"]<5)
    {
      $updateAttempts=$this->pdo->prepare("UPDATE Attempted_Logins SET login_attempts=:login_attempts WHERE ip_address=:ip_address");
      $updateAttempts->execute(array(":ip_address" => $dbIpUser["ip_address"],":login_attempts" => $dbIpUser["login_attempts"]+1));
      return true;
    }
    else
    {
      if($resetMinutes>=15)
      {
        $updateAttempts=$this->pdo->prepare("UPDATE Attempted_Logins SET login_attempts=0,last_login_time=:last_login_time WHERE ip_address=:ip_address");
        $updateAttempts->execute(array(":ip_address" => $dbIpUser["ip_address"],":last_login_time" => $this->getDateTime("now","Y-m-d H:i:s")));
        return true;
      }
      else
      {
        $updateAttempts=$this->pdo->prepare("UPDATE Attempted_Logins SET login_attempts=:login_attempts WHERE ip_address=:ip_address");
        $updateAttempts->execute(array(":ip_address" => $dbIpUser["ip_address"],":login_attempts" => $dbIpUser["login_attempts"]+1));
        return false;
      }
    }
  }
  private function setCookie($userInfo)
  {
    setcookie("rememberMe",$userInfo["authkey"],time() + (86400 * 14),"/");
  }
  public function userCookies($loginCookie,$action)
  {
    $cookieUser=$this->pdo->prepare("SELECT * FROM Users WHERE authkey=:authkey");
    $cookieUser->execute(array(":authkey" => $loginCookie));

    if($cookieUser->rowCount()===0)
      return false;
    else
    {
      if($action==="compare")
        return $cookieUser["authkey"];
      else if($action==="set")
        $this->setSessions($cookieUser,$this->findIP());
      else
        return null;
    }
  }
  private function setSessions($userDBinfo,$thisIP)
  {
    $_SESSION["loggedin"]=true;
    $_SESSION["username"]=$userDBinfo["username"];
    $_SESSION["userID"]=$userDBinfo["id"];
    $_SESSION["firstname"]=$userDBinfo["firstname"];
    $_SESSION["lastname"]=$userDBinfo["lastname"];
    $_SESSION["joindate"]=$userDBinfo["joindate"];

    $resetAttempts=$this->pdo->prepare("UPDATE Attempted_Logins SET login_attempts=0,last_login_time=:last_login_time WHERE ip_address=:ip_address");
    $resetAttempts->execute(array(":ip_address" => $thisIP,":last_login_time" => $this->getDateTime("now","Y-m-d H:i:s")));
  }
  private function getDateTime($when,$format)
  {
    $instance=new DateTime($when);
    if($format)
      return $instance->format($format);
    else
      return $instance;
  }
  private function findIP()
  {
    return $_SERVER["REMOTE_ADDR"];
  }
  public function newAuthKey($userID)
  {
    $resetAuthKey=$this->pdo->prepare("UPDATE Users SET authkey=:authkey WHERE id=:userid");
    $resetAuthKey->execute(array(":authkey" => bin2hex(openssl_random_pseudo_bytes(16)),":userid" => $userID));
  }
}
?>

I’m finding it hard to understand your code. For example, in your delayLogin, you check for previous attempts, and if there were 0 previous attempts, then delayLogin returns… true? A true value reads to me as: “Yes, delay login,” but you seem to treat it as the opposite.

I think you need to spend more time picking good names for things. Good naming goes a long way to making your code comprehensible, and it seems to be a recurring problem.

Also make sure your indentation is correct and consistent. Right now it isn’t, and that too makes it hard to read your code.

1 Like

The indentation is this forums fault - not my own. My copy of my code is perfect indentation. I don’t know how to fix it for here though.

I’m currently revisiting naming though.

It’s a known issue with Discourse… you need to leave an empty line between your text and the three back ticks that start your code block. I’ve edited your previous post and the indentation seems to be behaving itself now.

I thought I already do that though. I guess I’m missing the blank line though since I do do 3 ticks. I thought it just had to be on a new line when you did hte 3 ticks. Thanks for the formatting.

I don’t know how far you want to take OOP. But, I just recently learned that a lot of conditionals in a class, especially if you are using them to decipher flags, is a great sign you should be making use of polymorphism. This video explains what I mean.

Again, from an OOP perspective, your class is now responsible for too many things. delaying login attempts, counting attempts, setting cookies, setting sessions. You should probably be breaking the problem down even more and making classes for the different parts of solving the problem. You could also think from the perspective of things you might need in other parts of your application too, like sessions and cookies. They are prime candidates for their own classes.

I would also think using the user’s IP is a bad parameter for stopping brute force attacks (which the delay login method is for). If you have a group of users behind a single firewall (like most companies have), it would cause a lot of false positives. It would be better to use the username, if it is unique, which in most cases, it should be. Or, use the email address to log in, as it is unique.

Scott

1 Like

The username is unique (e-mail).

The way this works is if the IP is behaving badly, it doesn’t even give access to the Users table with the sensitive information. I realize it’s not the best way but from what I read, it’s the only way to actually grab users and hackers, and prevent bots from running 1 million scripts a minute.

What’s the point of different classes? Why can’t I keep everything in the UserServices class (noobie speaking.)

The more a class is broken down to the simplest core the easier it is to understand what role in plays in the entire script, not to mention making more portable. A good example of this is your pdo connection for that looks like it’s utilizing dependency interjection making it easier to swap that particular class out for another. Though to me the class doesn’t look that busy and maybe with some code optimization that goal could be achieved relatively easily.

Not sure what DI is but I do have autoloading (for classes) in place. Is that relevant at all?

Dependency Injection is what you already did with the PDO connection. The connection object is a dependency of your login class (i.e. your class needs it in order to function) and instead of instantiating it inside your class (which creates a tight coupling between the two) you’re passing it in (injecting it) through the constructor.

DI = Dependency Injection

Ah ok that makes sense. I still don’t see the point of breaking my class down further. I understand it’s to make it more readable/portable but technically the functions are all UserServices (login/logout, and soon registering.)

I suppose though when I do add registering, this will get to be a pretty big class. Eh, I guess I could make it into a Login / Logout / Register class (3 separate.) Not sure I agree with breaking up the “Login” class though since all the functions have purposes for logging in.

Check out SOLID principles. Having different classes to break down your problem into “things that needs changes for the same reasons” or “one reason for change” follow the “S” for SRP or Single Responsibility Principle.

It is a bit long winded from Uncle Bob, but watch the video.

This video is a bit more PHP related.

Now answer this question:

Would sessions and cookies have different “consumers” in your application? If yes, then they should take on their own responsibilities, because they would need to change for different reasons than your checkCredentials class.

I hope that makes sense. :blush:

Scott

1 Like

No - they should be the same person. If I’m understanding you right.

The cookie will only be activated if the user logging in has correct credentials, and if they selected the remember me feature. The cookie is populated with the authkey associated with each user account.

Upon logging out though, they get a new authkey in the database for security purposes.

I’ll watch those videos but I can’t right now. I’ll put them on my list of videos to watch. Promise I’ll get to it ASAP.

Edit- @cpradio . Just got an error upon posting “Error occured” - IIRC…Bounced back in like 1 second. Tried refreshing. Post wasn’t there. Tried submitting but it said “body was too similar to what was previously posted”. So I edited this comment on, submitted it, and it went through.

Consumer doesn’t mean the same person (although Uncle Bob does use that analogy), but rather parts of your application consuming those “things”. The better question is, will no other part of your application set or read data from a cookie (and not even the same cookie but A cookie)? Will no other part of your application need information from the session or even set information in the session?

Scott