Webwide is the inclusive discussion community for web designers, developers & makers.

Whether you're an enthusiast, in training, or a seasoned pro – you'll fit right in at Webwide. Creating an account is fast, easy and completely free so you can start participating right away.

Read our Code of Conduct

Free Membership Benefits

  • Participate in hundreds of interesting discussions
  • Network with industry peers and make new connections
  • Show off your own projects and relevant content
  • Get help and feedback for your coding and designs
  • Buy and sell services and resources in the marketplace
  • Participate in our friendly community challenges
  • Earn trophies and work your way up our leaderboards
  • Enjoy exclusive Webwide member discounts and offers
  • ...and so much more!

Help Wanted Help with auth class?

TGDesigns

Member
Joined
Nov 20, 2019
Messages
17
Reaction score
9
Points
5
As a self-taught reasonably new programmer the thing I find most daunting is security specifically security involving databases, I have written a user auth class but
I'm struggling to find a way to validate that it is secure. Is it ok to post it here for a peer review type thing? I only really need to know if there is any massive holes
and I don't want you guys to fix them I just want them pointed out!
 
  • Like
Reactions: ek1 and Adam

Gummibeer

Well-known member
Joined
Oct 5, 2019
Messages
664
Reaction score
518
Points
635
Age
26
Location
Hamburg, Germany
Local Time
Today, 22:55
Website
gummibeer.de
I only can recommend you to use proven and stable auth package, framework or service.
They will receive future updates and are most times battle proven.
If it's for learning I can try to help you if it's PHP. Have you implemented any standard? Or only a simple self thought implementation?
 

TGDesigns

Member
Joined
Nov 20, 2019
Messages
17
Reaction score
9
Points
5
I only can recommend you to use proven and stable auth package, framework or service.
They will receive future updates and are most times battle proven.
If it's for learning I can try to help you if it's PHP. Have you implemented any standard? Or only a simple self thought implementation?
Its mostly for learning its pretty simple self implementation ill get some code posted!
 

TGDesigns

Member
Joined
Nov 20, 2019
Messages
17
Reaction score
9
Points
5
PHP:
require 'db.php';
session_start(); ///needs an open session to set the logged in value
class User {
    public $username = null;
    public $password = null;
    public $name = null;
    public $email = null;
    public $ftp = null;
    public $connection = null;
    public $errors = array();
    public $errorFlag = false;
    public $lastInsertedID = null;
    
    function __construct() {
        $this->connection = connect_db(); ///this is the basic database connection
        
    }
    //Get the errors array.
    public function getErrors() {
        return $this->errors;
    }
    public function storeFormValues($username, $password, $name, $email) {
        if (empty($password)) {
            array_push($this->errors, "Please Enter a Password!");
            $this->errorFlag = true;
        } else {
            if (!preg_match('/(?=.*[a-z])(?=.*\d).{5,}/i', $password)) {
                array_push($this->errors, "Passwords must have a minimum of 5 characters and contain at least one letter and at least on number");
                $this->errorFlag = true;
            } else {
                $this->password = password_hash($password, PASSWORD_DEFAULT);
            }
        }
        if (empty($username)) {
            array_push($this->errors, "Please Enter a Username!");
            $this->errorFlag = true;
        } else {
            if (!preg_match("/^[a-zA-Z ]*$/", $username)) {
                array_push($this->errors, "Only letters and white space allowed in Username");
                $this->errorFlag = true;
            } else {
                $this->username = $username;
            }
        }
        if (empty($name)) {
            array_push($this->errors, "Please Enter a name!");
            $this->errorFlag = true;
        } else {
            if (!preg_match("/^[a-zA-Z ]*$/", $name)) {
                array_push($this->errors, "Only letters and white space allowed in Name!");
                $this->errorFlag = true;
            } else {
                $this->name = $name;
            }
        }
        if (empty($email)) {
            array_push($this->errors, "Please Enter a Email!");
            $this->errorFlag = true;
        } else {
            if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
                array_push($this->errors, "Invalid Email format");
                $this->errorFlag = true;
            } else {
                $this->email = $email;
            }
        }
        if ($this->errorFlag === false) {
            $this->ftp = hash('sha512', rand());
            return true;
        } else {
            return false;
        }
    }
    public function logout() {
        session_destroy();
        header("Location: ../../login.php"); ///destroys all session data and redirects to login
        
    }
    public function userLogin($email, $password) {
        try {
            $sql = "SELECT * FROM accounts where account_email = ?";
            $stmt = $this->connection->prepare($sql);
            $stmt->execute(array($email));
            if ($stmt->rowCount() > 0) {
                $output = $stmt->fetch();
                $hash = $output['account_password'];
                if (password_verify($password, $hash)) {
                    header_remove();
                    header('Location: ./home.php');
                    $_SESSION['loggedIn'] = 1;
                    $_SESSION['username'] = $output['account_username'];
                    $_SESSION['id'] = $output['account_id'];
                    return;
                } else {
                    $_SESSION['loggedIn'] = 0;
                    array_push($this->errors, "User password didn't match!");
                    return false;
                }
            } else {
                array_push($this->errors, "User Not Found!");
                return false;
            }
        }
        catch(PDOException $e) {
            //$e->getMessage();
            array_push($this->errors, "Unknown Error");
            return false;
        }
    }
    public function userRegister() {
        //below is the basic code to add a user to the database
        try {
            $sql = "INSERT INTO accounts (account_name, account_password, account_username, account_email, ftp_password, is_admin, is_enabled, is_ftp_enabled) VALUES (?,?,?,?,?,?,?,?)";
            $stmt = $this->connection->prepare($sql);
            $stmt->execute([$this->name, $this->password, $this->username, $this->email, $this->ftp, "0", "1", "1"]);
            $this->lastInsertedID = $this->connection->lastInsertId(); //this gets the id of thew last inserted row so it can then init all the other needed databases
        }
        catch(PDOException $e) {
            array_push($this->errors, "We already have an account with that email!");
            $this->errorFlag = true;
        }
        
        
        //The two insert statments below setup the default values for the ship station config saving
        try {
            $sql = "INSERT INTO configuration (`account_id`, `shipstationPublic`, `shipstationPrivate`) VALUES (?,?,?)"; //used backticks for escaping reserved words
            $stmt = $this->connection->prepare($sql);
            $stmt->execute([ $this->lastInsertedID, "0", "0"]);
        }
        catch(PDOException $e) {
            if ($e->errorInfo[1] == 1062) {
                array_push($this->errors, "Configuration Already set!");
                $this->errorFlag = true;
             } else {
                array_push($this->errors, $e);
                $this->errorFlag = true;
             }
        }
        
         if ($this->errorFlag === false) {
            return true;
        } else {
            return false;
        }
    }
    public function getUserData($userID){
        try{
            $sql = "SELECT * FROM accounts where account_id = ?";
            $stmt = $this->connection->prepare($sql);
            $stmt->execute(array($userID));
            if ($stmt->rowCount() > 0) {
                $output = $stmt->fetch();
                return array("email"=>$output['account_email'], "username"=>$output['account_username'], "name"=>$output['account_name']);;
            }else{
                return false;
            }
            
        }       
        catch(PDOException $e) {
            array_push($this->errors, "Could not get user information");
            return false;
        }
    }
    
}
So this is the basic class the db.php file is just a simple database connection/config file. As I say its more so I can understand and I'm not actually using this in my application!
 

Gummibeer

Well-known member
Joined
Oct 5, 2019
Messages
664
Reaction score
518
Points
635
Age
26
Location
Hamburg, Germany
Local Time
Today, 22:55
Website
gummibeer.de
At super first a simple cleaned-up and fixed code:

PHP:
<?php

require 'db.php';

// needs an open session to set the logged in value
session_start();

class User
{
    private $username = null;
    private $password = null;
    private $name = null;
    private $email = null;
    private $ftp = null;
    private $connection = null;
    private $errors = [];
    private $lastInsertedID = null;

    public function __construct()
    {
        $this->connection = connect_db(); // this is the basic database connection
    }

    public function getErrors(): array
    {
        return $this->errors;
    }

    public function storeFormValues($username, $password, $name, $email): bool
    {
        if (empty($password)) {
            array_push($this->errors, "Please Enter a Password!");
        } else {
            if (!preg_match('/(?=.*[a-z])(?=.*\d).{5,}/i', $password)) {
                array_push($this->errors, "Passwords must have a minimum of 5 characters and contain at least one letter and at least on number");
            } else {
                $this->password = password_hash($password, PASSWORD_DEFAULT);
            }
        }

        if (empty($username)) {
            array_push($this->errors, "Please Enter a Username!");
        } else {
            if (!preg_match("/^[a-zA-Z ]*$/", $username)) {
                array_push($this->errors, "Only letters and white space allowed in Username");
            } else {
                $this->username = $username;
            }
        }

        if (empty($name)) {
            array_push($this->errors, "Please Enter a name!");
        } else {
            if (!preg_match("/^[a-zA-Z ]*$/", $name)) {
                array_push($this->errors, "Only letters and white space allowed in Name!");
            } else {
                $this->name = $name;
            }
        }

        if (empty($email)) {
            array_push($this->errors, "Please Enter a Email!");
        } else {
            if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
                array_push($this->errors, "Invalid Email format");
            } else {
                $this->email = $email;
            }
        }

        if (empty($this->errors)) {
            $this->ftp = hash('sha512', rand());

            return true;
        }

        return false;
    }

    public function logout()
    {
        session_destroy();
        header("Location: ../../login.php"); // destroys all session data and redirects to login
    }

    public function userLogin($email, $password): bool
    {
        try {
            $sql = "SELECT * FROM accounts where account_email = ?";
            $stmt = $this->connection->prepare($sql);
            $stmt->execute([$email]);

            if ($stmt->rowCount() > 0) {
                $output = $stmt->fetch();
                $hash = $output['account_password'];

                if (password_verify($password, $hash)) {
                    header_remove();
                    header('Location: ./home.php');
                    $_SESSION['loggedIn'] = 1;
                    $_SESSION['username'] = $output['account_username'];
                    $_SESSION['id'] = $output['account_id'];

                    return true;
                } else {
                    $_SESSION['loggedIn'] = 0;
                    array_push($this->errors, "User password didn't match!");

                    return false;
                }
            } else {
                array_push($this->errors, "User Not Found!");

                return false;
            }
        } catch (PDOException $e) {
            array_push($this->errors, "Unknown Error");

            return false;
        }
    }

    public function userRegister(): bool
    {
        // below is the basic code to add a user to the database
        try {
            $sql = "INSERT INTO accounts (account_name, account_password, account_username, account_email, ftp_password, is_admin, is_enabled, is_ftp_enabled) VALUES (?,?,?,?,?,?,?,?)";
            $stmt = $this->connection->prepare($sql);
            $stmt->execute([$this->name, $this->password, $this->username, $this->email, $this->ftp, "0", "1", "1"]);
            $this->lastInsertedID = $this->connection->lastInsertId(); //this gets the id of thew last inserted row so it can then init all the other needed databases
        } catch (PDOException $e) {
            array_push($this->errors, "We already have an account with that email!");
        }


        // The two insert statements below setup the default values for the ship station config saving
        try {
            $sql = "INSERT INTO configuration (`account_id`, `shipstationPublic`, `shipstationPrivate`) VALUES (?,?,?)"; //used backticks for escaping reserved words
            $stmt = $this->connection->prepare($sql);
            $stmt->execute([$this->lastInsertedID, "0", "0"]);
        } catch (PDOException $e) {
            if ($e->errorInfo[1] == 1062) {
                array_push($this->errors, "Configuration Already set!");
            } else {
                array_push($this->errors, $e);
            }
        }

        return empty($this->errors);
    }

    public function getUserData($userID): ?array
    {
        try {
            $sql = "SELECT * FROM accounts where account_id = ?";
            $stmt = $this->connection->prepare($sql);
            $stmt->execute(array($userID));

            if ($stmt->rowCount() > 0) {
                $output = $stmt->fetch();

                return [
                    "email" => $output['account_email'],
                    "username" => $output['account_username'],
                    "name" => $output['account_name']
                ];
            }
        } catch (PDOException $e) {
            array_push($this->errors, "Could not get user information");
        }

        return null;
    }
}
Only small changes but your git/VCS should tell you what has changed.
 
Last edited:

Gummibeer

Well-known member
Joined
Oct 5, 2019
Messages
664
Reaction score
518
Points
635
Age
26
Location
Hamburg, Germany
Local Time
Today, 22:55
Website
gummibeer.de
You should only store the user_id in the session - nothing more auth related. To verify if the user is logged in you should query your database with this ID.
At the end this part, which is the most important one, is missing. How you verify that the user is logged in or not.
 

TGDesigns

Member
Joined
Nov 20, 2019
Messages
17
Reaction score
9
Points
5
Thanks I appreciate it!

So Essentially store a "logged in true/false" flag in the database and use the user idea to check that?

Also, the only thing I'm a little confused about with the code is:

PHP:
public function getUserData($userID): ?array
What the ": ?array" does after the function declaration I've never encountered that before.
 

Gummibeer

Well-known member
Joined
Oct 5, 2019
Messages
664
Reaction score
518
Points
635
Age
26
Location
Hamburg, Germany
Local Time
Today, 22:55
Website
gummibeer.de
No flag at all - you store the user ID in the session $_SESSION['user_id'] = $user['id']. If the user comes back and you verify the user logged in state you take the ID that you've stored in the session and query your DB if an user with given ID exists.

The ?array says that this method will return an array or null. The ? is a nullable type hint introduced in PHP7.1

All the type hints are added for better type security and to force you to return only one type per method (or null).

If you want to see a battle proven solution in full OOP you should check out the Laravel Session Auth process.


Classes you should check are:

Login process:
  • \App\Http\Controllers\Auth\LoginController => \Illuminate\Foundation\Auth\AuthenticatesUsers::login()
  • \Illuminate\Foundation\Auth\AuthenticatesUsers::attemptLogin()
  • \Illuminate\Auth\SessionGuard::attempt()
  • \Illuminate\Auth\DatabaseUserProvider::retrieveByCredentials()
  • \Illuminate\Auth\DatabaseUserProvider::validateCredentials()
  • \Illuminate\Auth\SessionGuard::login()
  • \Illuminate\Auth\SessionGuard::updateSession()

Authenticate process:
  • \App\Http\Middleware\Authenticate
  • \Illuminate\Auth\Middleware\Authenticate::authenticate()
  • \Illuminate\Auth\GuardHelpers::check()
  • \Illuminate\Auth\SessionGuard::user()

It's all relative simple code. You can ignore all the events and request throttle logic. You will find a lot of interfaces by simply clicking through the methods in your IDE - it's because you can customize everything in Laravel. It provides a Token auth and also a request based auth. But the stateful session one is the one that you will want to rebuild. 😉
 
  • Love
  • Like
Reactions: Adam and TGDesigns

TGDesigns

Member
Joined
Nov 20, 2019
Messages
17
Reaction score
9
Points
5
Thats perfect thats just what im looking for! Sometimes its hard to find examples without "just learn larvel/codeignitor" when I just want an oop login class! Thanks very much!
 
  • Like
Reactions: Gummibeer

Gummibeer

Well-known member
Joined
Oct 5, 2019
Messages
664
Reaction score
518
Points
635
Age
26
Location
Hamburg, Germany
Local Time
Today, 22:55
Website
gummibeer.de
Thats perfect thats just what im looking for! Sometimes its hard to find examples without "just learn larvel/codeignitor" when I just want an oop login class! Thanks very much!
The Laravel example gives you also some insights and ideas how to split your OOP code and get near SRP. So the differentiation of request/form data, session, guard, user model, middleware and so on. If your project grows it gets more and more important to have splitted code/classes to call them independently and also replace single parts without updating all usage lines. Like the session or guard driver. That's what Laravel uses interfaces, service container and manager driver pattern for.
 

TGDesigns

Member
Joined
Nov 20, 2019
Messages
17
Reaction score
9
Points
5
Just to get it straight in my head, I store the user id in the session and redirect them to the "logged in" page, then if they come back and the session still had a user ID I check the user id is valid and redirect to the logged in page if it is. Then I guess I set an expiry time?

Sorry if the questions quite simple I just wanna make sure I understand correctly before I move on, no good learning something half heartedly!