Login function with PDO

This login function below is working perfectly if I don’t hash my password:

function Login_Attempt($UserName,$Password){
          global $ConnectingDB;
          $sql = "SELECT * FROM admins WHERE username=:userName AND password=:passWord LIMIT 1";
          $stmt = $ConnectingDB->prepare($sql);
          
            $stmt->bindValue(':userName',$UserName);
            $stmt->bindValue(':passWord',$Password);
            $stmt->execute();
            $Result = $stmt->rowcount();
            if ($Result==1) {
              return $Found_Account=$stmt->fetch();
            }else {
              return null;
            } 
          
        }

but when I hashed my password, I was trying to verify the user submitted password $Password with $storedPwd in the database before binding username and password like this:

function Login_Attempt($UserName,$Password){
  global $ConnectingDB;
  $sql = "SELECT * FROM admins WHERE username=:userName AND password=:passWord LIMIT 1";
  $stmt = $ConnectingDB->prepare($sql);
  while ($DataRows = $stmt->fetch()) {
    $username = $DataRows["username"];
    $storedPwd = $DataRows["password"];
  }
  $compare_password = password_verify($Password, $storedPwd);
  if($compare_password == $Password){
    $stmt->bindValue(':userName',$UserName);
    $stmt->bindValue(':passWord',$storedPwd);
    $stmt->execute();
    $Result = $stmt->rowcount();
    if ($Result==1) {
      return $Found_Account=$stmt->fetch();
    }else {
      return null;
    } 
  }else {
    echo "Bad request";
  }
}

My problem is I don’t know whether the command that I used to fetch username and password was right before binding them together or if I should not bind them again once I fetch the username and password from the database, so that the Login function can work as the one above.

I’m rewriting my login page this is what I have come up with so far (it hasn’t been test yet) →

I’ll just show you my database connection class →

<?php


namespace PhotoTech;

use mysql_xdevapi\Exception;
use PDO;
use PDOException;

class Database {
    private PDO $connection;

    public function __construct()
    {
        try {
            $db_options = [
                PDO::ATTR_EMULATE_PREPARES => false,
                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
            ];
            $this->connection = new PDO('mysql:host=' . DATABASE_HOST . ';dbname=' . DATABASE_NAME . ';charset=utf8', DATABASE_USERNAME, DATABASE_PASSWORD, $db_options);
        } catch (PDOException $e) {
            if ($e->errorInfo[1] === 1045) {
                echo "Can't Connect to Database " . $e->errorInfo[1] . "<br>";
                return false;
            }

            throw $e;
        } catch (Exception $e) {
            echo 'Caught exception: ', $e->getMessage(), "\n";
        }
        return true;
    }

    public function getConnection(): PDO
    {
        return $this->connection;
    }
}

and this is what I have so far for my login class →

<?php

namespace PhotoTech;

use PDO;
use Exception;
use JetBrains\PhpStorm\Pure;
use DateTime;
use DateTimeZone;

class LoginRepository {

    private Database $database;
    private string $table = 'admins'; // Replace with your actual table name

    public function __construct(Database $database)
    {
        $this->database = $database;
    }

    public function verify_credentials($username, $password)
    {
        $sql = "SELECT id, password FROM " . $this->table . " WHERE username =:username LIMIT 1";
        $user = $this->retrieve_credentials($sql, $username);
        if ($user && password_verify($password, $user['password'])) {
            unset($user['password']);
            session_regenerate_id(); // prevent session fixation attacks
            $_SESSION['user_id'] = $user['id'];
        }


    }

    protected function retrieve_credentials(string $sql, string $username): ?array
    {
        $pdo = $this->database->getConnection();
        $stmt = $pdo->prepare($sql);

        $stmt->execute([ 'username' => $username]);
        $result = $stmt->fetch(PDO::FETCH_ASSOC);
        return $result !== false ? $result : null;
    }

    public function store_token_in_database($user_id, $token) {

    }

}

Maybe this will help some? :thinking:

Just an FYI: OP has a growing thread on another forum.

@Strider64, you might want to check out my “Clean PDO” example.

A few comments on the code you posted…

Do not output internal system errors to the user. The information is useless to them and only good for hackers.

You don’t need the getConnection method if you rework the Database class a bit.

The double catch isnt needed. Any Exception for the DB connection is not a recoverable error. You should just log it (Not output it to the user.) If you want to provide a “nice” error message to the user you can use set_exemption_handler and take care of it there. There are many reasons a DB connection can fail, so randomly just coding for a 1045 error just litters the code base.

In your verify_credentials method, the LIMIT 1 is pointless. If you have a unique constraint on the username column as you should there can only ever be one result.

There is no need to unset the password. It is not accessible outside of that method.

The method does not handle the case of an invalid username and/or password

On the plus side, you are using Dependency Injection to pass the DB connection to the LoginRepository

I know you frown up double catch but wouldn’t the following be ok?

class ErrorHandler
{
    public function handlePDOException(PDOException $e)
    {
        error_log('PDO Error: ' . $e->getMessage());
        echo 'An error occurred while connecting to the database.';
    }

    public function handleGeneralException(Exception $e)
    {
        error_log('General Error: ' . $e->getMessage());
        echo 'An error occurred.';
    }
}

class MyClass
{
    private ErrorHandler $errorHandler;

    public function __construct(ErrorHandler $errorHandler)
    {
        $this->errorHandler = $errorHandler;
    }

    public function performAction()
    {
        try {
            // Some code that may throw exceptions
        } catch (PDOException $e) {
            $this->errorHandler->handlePDOException($e);
        } catch (Exception $e) {
            $this->errorHandler->handleGeneralException($e);
        }
    }
}

I fix the method and added to class

```class LoginRepository {

    private Database $database;
    private string $table = 'admins'; // Replace with your actual table name

    public function __construct(Database $database)
    {
        $this->database = $database;
    }

    public function verify_credentials($username, $password): bool
    {
        $sql = "SELECT id, password FROM " . $this->table . " WHERE username =:username LIMIT 1";
        $user = $this->retrieve_credentials($sql, $username);
        if ($user && password_verify($password, $user['password'])) {
            session_regenerate_id(); // prevent session fixation attacks
            $_SESSION['user_id'] = $user['id'];
            return true;
        }

        return false;
    }


    protected function retrieve_credentials(string $sql, string $username): ?array
    {
        $pdo = $this->database->getConnection();
        $stmt = $pdo->prepare($sql);

        $stmt->execute([ 'username' => $username]);
        $result = $stmt->fetch(PDO::FETCH_ASSOC);
        return $result !== false ? $result : null;
    }

    public function store_token_in_database($user_id, $token) {
        $pdo = $this->database->getConnection();
        $sql = "UPDATE ". $this->table . " SET token=:token WHERE id=:id";
        $stmt= $pdo->prepare($sql);
        $stmt->execute(['token' => $token, 'id' => $user_id]);
        return true;
    }

}

This is how I use it in the php main file

/ Verify the username and password
if ($loginRepository->verify_credentials($username, $password)) {
    // Generate a unique token
    $token = bin2hex(random_bytes(32));

    // Store the token in the user's database record (or other persistent storage mechanism)
    $loginRepository->store_token_in_database($_SESSION['user_id'], $token);

    // Set a cookie with the token and a 6-month expiration time
    setcookie('login_token', $token, [
        'expires' => strtotime('+6 months'),
        'path' => '/',
        'domain' => 'www.phototechguru.com',
        'secure' => true,
        'httponly' => true,
        'samesite' => 'Lax'
    ]);

    // Store the token in the user's session
    $_SESSION['login_token'] = $token;
    //echo 'It works!' . $token . "<br>";
    // Redirect the user to the dashboard or home page
    //header('Location: dashboard.php');
    //exit;
} else {
    // Invalid username or password
    $error = 'Invalid username or password';
}

So I could something like the following doing away with the getConnection method?


namespace FanOfLEGO;

use PDO;
use PDOException;

class Database extends PDO
{
    private static ?Database $_instance = null;

    protected static function getInstance(): Database
    {
        if (!self::$_instance) {
            self::$_instance = new self();
        }
        return self::$_instance;
    }

    public static function pdo(): PDO
    {
        return static::getInstance();
    }

    private function __construct()
    {
        $dsn = 'mysql:host=' . DATABASE_HOST . ';dbname=' . DATABASE_NAME . ';charset=utf8mb4';
        $options = [
            PDO::ATTR_EMULATE_PREPARES => false,
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ];

        try {
            parent::__construct($dsn, DATABASE_USERNAME, DATABASE_PASSWORD, $options);
        } catch (PDOException $e) {
            // Handle the exception as needed, for example, by logging the error
            // and displaying a user-friendly error message
            throw $e;
        }
    }

    private function __clone()
    {
        // Empty clone magic method to prevent duplication
    }
}```

and  this as an example


```namespace FanOfLEGO;

use PDO;

class UserRepository
{
    private Database $database;

    public function __construct(Database $database)
    {
        $this->database = $database;
    }

    public function getUserById(int $id): ?array
    {
        $stmt = $this->database->prepare("SELECT * FROM users WHERE id = :id");
        $stmt->execute(['id' => $id]);
        $result = $stmt->fetch(PDO::FETCH_ASSOC);

        return $result !== false ? $result : null;
    }

    // Add more methods to interact with the user table as needed
}

Thanks very much, I love the way you really elaborate on this topic. Thanks once again, I am very grateful.

Hi @Strider64,

The latest code is off in a whole other direction. Lets go to the basics. Look at your code. You see the code duplication you have in just a few classes? Now Imagine how much that will grow over the course of an entire application. I am sure you know duplicating code is not good. Focus your attention on what you can do about that and lets see what you come up with. Hint: I already gave you one example of what you can do.

Sorry, it was late, tired and I was having problems posting code.

I just start with one thing →

Database Class →

<?php

namespace FanOfLEGO;

use PDO;
use PDOException;

class Database extends PDO
{
    private static ?Database $_instance = null;

    protected static function getInstance(): Database
    {
        if (!self::$_instance) {
            self::$_instance = new self();
        }
        return self::$_instance;
    }

    public static function pdo(): PDO
    {
        return static::getInstance();
    }

    private function __construct()
    {
        $dsn = 'mysql:host=' . DATABASE_HOST . ';dbname=' . DATABASE_NAME . ';charset=utf8mb4';
        $options = [
            PDO::ATTR_EMULATE_PREPARES => false,
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ];

        try {
            parent::__construct($dsn, DATABASE_USERNAME, DATABASE_PASSWORD, $options);
        } catch (PDOException $e) {
            // Handle the exception as needed, for example, by logging the error
            // and displaying a user-friendly error message
            throw $e;
        }
    }

    private function __clone()
    {
        // Empty clone magic method to prevent duplication
    }
}

This is just an example in the User Repository class on on the implementation →

<?php

namespace FanOfLEGO;

use PDO;

class UserRepository
{
    private Database $database;

    public function __construct(Database $database)
    {
        $this->database = $database;
    }
    
    // Grab user's information by id
    public function getUserById(int $id): ?array
    {
        $stmt = $this->database->prepare("SELECT * FROM users WHERE id = :id");
        $stmt->execute(['id' => $id]);
        $result = $stmt->fetch(PDO::FETCH_ASSOC);

        return $result !== false ? $result : null;
    }

    // Add more methods to interact with the user table as needed
}

and I can use the my application as follows:

<?php

require_once "Database.php";
require_once "UserRepository.php";

use FanOfLEGO\Database;
use FanOfLEGO\UserRepository;

$database = Database::pdo();
$userRepository = new UserRepository($database);

// Get a user by their ID
$user = $userRepository->getUserById(1);
if ($user) {
    echo "User found: " . $user['username'];
} else {
    echo "User not found.";
}

// Add more code to interact with the UserRepository as needed

I will start a new thread on the try-catch as I really butcher that one. It’s hard getting old at times. :rofl:

Sponsor our Newsletter | Privacy Policy | Terms of Service