IMPORTANT
After following the advice in the answers below, the client was able to log-in without any problems but did not attempt to actually navigate the secured pages. When he attempted to do so later, he was returned to log-in as before with the "Please log in" error. After much head scratching, something incredibly simple came to mind - the client was accessing the site with http://www.example.com/admin and everything in the login script was redirecting to http://example.com, so the session cookie that it was looking for was set for another domain. This also explains why he had problems logging in the first time but not subsequent times - the script redirected him to the log-in form without the www.
A quick fix was to write a .htaccess file to remove the www, problem solved. Of course this could also be handled within the login script, which I will improve for future use.
ORIGINAL POST
I develop PHP and MySQL based sites with a home brew CMS and log-in system. My CMS is unique to every client and it has been quite the crowd pleaser - unfortunately the same is not true of my log-in system. The following is a long post but I need to cover the details to try and find a solution. Bear with me..
The system is fairly straight forward, if not a bit hefty. Each user has a salted hash stored in a MySQL table, alongside the SALT. When the user logs in, their SALT is retrieved and the submitted password becomes a salted hash.
If the submitted salted hash matches the one stored in the table, the user is authenticated. Details such as their name, last IP address, and account level (3 levels on most sites) are stored in an array assigned to a session variable. They are then redirected to the landing page of the restricted site to which they logged in (Members Only or Admin/CMS).
Secured pages include a smaller auth.php file that checks to see if the session variable containing their details is present. If not, they are redirected to that site's log-in form with an error message that reads "Please log-in." If it is present, they are allowed to continue and the details stored in the array are assigned to variables.
The problem that many users has reported is that they often need to log-in several times to keep from being bounced back to the log-in form with the "Please log-in" error message, or they navigate to another page in the secure site and randomly get bounced back to login with the same error. So, the session variable seems to either not be getting set or it is being cleared for some reason during normal use of the site.
The first problem has NEVER happened to me - over a multitude of devices and networks - and I have witnessed it at a client's office using their laptop. I had them connect to my mobile hotspot and there was no change. However, they were able to log in without any problems using my laptop and my hotspot connection. Unfortunately, I was not able to connect to their network using my laptop, so that variable could not be ruled out.
*NOTE - * I forgot to mention initially that the system works normally for problem clients after they have logged in two or three times with the correct credentials. Subsequent log-in attempts while their browser remains open tend to execute without problems thereafter. Also, the log-in page destroys the session.
Here is the code for each stage, starting with the log-in script:
login.php
<?php
putenv("TZ=US/Eastern");
if (array_key_exists('site', $_POST)) {
$authenticate = new loginUser($_POST['username'], $_POST['password'], $_POST['site'], $_SERVER['REMOTE_ADDR']);
}
//Authenticate and log-in
class loginUser {
private $memDB, $username, $password, $site, $ip_address;
//Clean input variables
private function clean($str) {
$str = @trim($str);
if(get_magic_quotes_gpc()) {
$str = stripslashes($str);
}
return $str;
}
//Construct variables
function __construct($username, $password, $site, $ip_address) {
session_start();
$this->memDB = new PDO('mysql:host=localhost;dbname=exampleDB', 'exampleUser', 'examplePassword');
$this->username = $this->clean($username);
$this->password = $this->clean($password);
$this->site = $site;
$this->ip_address = $ip_address;
$this->authUser();
}
//Validate username
private function validateUsername($username) {
$checkUsername = $this->memDB->prepare("SELECT COUNT(*) FROM accounts WHERE username = ?");
$checkUsername->execute(array($username));
return $checkUsername->fetchColumn();
}
//Obtain and set account details
private function accountDetails() {
$fetchAccountDetails = $this->memDB->prepare("SELECT id, name_f, name_l, ipAddr, lastLogin, accountLevel, isActive
FROM accounts WHERE username = ?");
$fetchAccountDetails->execute(array($this->username));
$accountDetails = $fetchAccountDetails->fetch();
$this->updateLogin();
return $accountDetails;
}
//Update last login details
private function updateLogin() {
$updateLogin = $this->memDB->prepare("UPDATE accounts SET ipAddr = ?, lastLogin = DATE_ADD(NOW(), INTERVAL 1 HOUR) WHERE username = ?");
$updateLogin->execute(array($this->ip_address, $this->username));
}
public function authUser() {
$loginErr = array(); //Array for holding login error message
$loginErrFlag = false; //Boolean for error
//Validate submitted $_POST elements
if (!$this->username) {
$loginErr[] = "Username missing";
$loginErrFlag = true;
}
if (!$this->password) {
$loginErr[] = "Password missing";
$loginErrFlag = true;
}
if ($this->username && $this->validateUsername($this->username) == 0) {
$loginErr[] = "Username invalid";
$loginErrFlag = true;
}
if (!$loginErrFlag) {
//Fetch the password and SALT to compare to entered password
$validatePW = $this->memDB->prepare("SELECT password, salt FROM accounts WHERE username = ? LIMIT 1");
$validatePW->execute(array($this->username));
$passwordResult = $validatePW->fetch();
$dbPW = $passwordResult['password'];
$dbSalt = $passwordResult['salt'];
//Compare entered password to SALT + hash
$hashPW = hash('sha512', $dbSalt . $this->password);
if ($hashPW === $dbPW) {
//Logged in
$_SESSION['CVFD-USER-DETAILS'] = $this->accountDetails();
//Redirect to secure landing page for log-in origin (Members or Admin)
//Adding SID is a recent attempt to handle log-in problems
header("Location: http://example.com/$this->site/$this->site-main.php?" . SID);
//session_write_close() was here but was removed
exit();
} else {
//Password invalid
$loginErr[] = "Please check your password and try again";
$_SESSION['CVFD_LOGIN_ERR'] = $loginErr;
//Redirect to the log-in for the origin
header("Location: http://example.com/$this->site");
session_write_close();
exit();
}
} else {
$_SESSION['CVFD_LOGIN_ERR'] = $loginErr;
header("Location: http://example.com/$this->site");
session_write_close();
exit();
}
}
}
?>
auth.php
<?php
session_start();
if (!isset($_SESSION['CVFD-USER-DETAILS']) || $_SESSION['CVFD-USER-DETAILS'] == '') {
//Not logged in
$_SESSION['CVFD_LOGIN_ERR'] = array('Please login');
header('Location: http://example.com/members');
session_write_close();
exit();
} else {
$userDetails = $_SESSION['CVFD-USER-DETAILS']; //Assign user details array to variable
//Check to see if account is active
$accountStatus = $userDetails['isActive'];
$accountLevel = $userDetails['accountLevel'];
if ($accountStatus == 0) {
//Account is not yet active (pending Admin activation)
$_SESSION['CVFD_LOGIN_ERR'] = array('Your account is suspended or pending activation');
header('Location: http://example.com/members');
session_write_close();
exit();
} else {
$CVFDFirstName = $userDetails['name_f'];
$CVFDLastName = $userDetails['name_l'];
$CVFDLastLogin = date("m/d/Y H:i:s", strtotime($userDetails['lastLogin']));
$CVFDAccountLevel = $userDetails['accountLevel'];
$CVFDIPAddr = $userDetails['ipAddr'];
}
}
?>
Here is how the auth.php is included in secure files-
<?php
if (substr_count($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip')) ob_start("ob_gzhandler"); else ob_start();
require($_SERVER['DOCUMENT_ROOT'] . '/members/includes/handlers/handler.auth.php');
Any help would be appreciated. Quite a mystery..
Thanks!