Login Script with ‘Remember Me’ feature will allow the user to preserve their logged in status. When the user checks the Remember Me option, then the logged in status is serialized in the PHP session or cookies like storages.
While writing user login data in the session or cookie we need to be aware of the security breaches which might compromise the application’s authentication system. Plain passwords should not be stored in the user’s cookie, this will allow hacking the application.
This example will help you to build a persistent authentication system for your PHP web application. When the user attempts to log in with the application, the entered login credentials are verified with the database.
If a match is found, the PHP session and the cookies are used to preserve user logged-in state before redirecting the user to the dashboard. On successful login, the unique member id from the member database is stored in a session.
Then, the cookies are set to keep the login name and the password for a specified expiration period. Instead of storing the users’ plain password, random password and token are generated and stored in the cookie to avoid hacking.
When the user accessing the application pages, the existing logged in session is checked to redirect the user to access the requested page. If the session is empty, then the code will check the logged-in with the cookies. If both the session and the cookies are not having any data about the remembered login, then the user will be redirected back to the login page.
This screenshot shows the UI for the secured Remember Me with a login form.
The authentication cookies are set with the expiration time of 1 month. The random password and tokens will be stored in the database with the expiration date and time. The cookie-based logged in state validation is done by testing cookie availability and expiration stored in the database.
I have created a login form to get the username and password. This form contains a checkbox captioned as ‘Remember Me’ to allow the user to preserve his logged in status. When the user submits the login data, the posted details are received in PHP and validated with the member database.
On successful login, if the user selected ‘Remember Me’ then the logged-in status is stored in PHP session and cookies.
As it is a security loophole to store the plain password in the cookie, the random numbers are generated as the authentication keys. These keys are hashed and stored in the database with an expiration period of 1 month. Once the time expires, then the expiration flag will be set to 0 and the keys will be deactivated.
<?php
session_start();
require_once "Auth.php";
require_once "Util.php";
$auth = new Auth();
$db_handle = new DBController();
$util = new Util();
require_once "authCookieSessionValidate.php";
if ($isLoggedIn) {
$util->redirect("dashboard.php");
}
if (! empty($_POST["login"])) {
$isAuthenticated = false;
$username = $_POST["member_name"];
$password = $_POST["member_password"];
$user = $auth->getMemberByUsername($username);
if (password_verify($password, $user[0]["member_password"])) {
$isAuthenticated = true;
}
if ($isAuthenticated) {
$_SESSION["member_id"] = $user[0]["member_id"];
// Set Auth Cookies if 'Remember Me' checked
if (! empty($_POST["remember"])) {
setcookie("member_login", $username, $cookie_expiration_time);
$random_password = $util->getToken(16);
setcookie("random_password", $random_password, $cookie_expiration_time);
$random_selector = $util->getToken(32);
setcookie("random_selector", $random_selector, $cookie_expiration_time);
$random_password_hash = password_hash($random_password, PASSWORD_DEFAULT);
$random_selector_hash = password_hash($random_selector, PASSWORD_DEFAULT);
$expiry_date = date("Y-m-d H:i:s", $cookie_expiration_time);
// mark existing token as expired
$userToken = $auth->getTokenByUsername($username, 0);
if (! empty($userToken[0]["id"])) {
$auth->markAsExpired($userToken[0]["id"]);
}
// Insert new token
$auth->insertToken($username, $random_password_hash, $random_selector_hash, $expiry_date);
} else {
$util->clearAuthCookie();
}
$util->redirect("dashboard.php");
} else {
$message = "Invalid Login";
}
}
?>
This the HTML code to display the login form with ‘Remember Me’ option.
<form action="" method="post" id="frmLogin">
<div class="error-message"><?php if(isset($message)) { echo $message; } ?></div>
<div class="field-group">
<div>
<label for="login">Username</label>
</div>
<div>
<input name="member_name" type="text"
value="<?php if(isset($_COOKIE["member_login"])) { echo $_COOKIE["member_login"]; } ?>"
class="input-field">
</div>
</div>
<div class="field-group">
<div>
<label for="password">Password</label>
</div>
<div>
<input name="member_password" type="password"
value="<?php if(isset($_COOKIE["member_password"])) { echo $_COOKIE["member_password"]; } ?>"
class="input-field">
</div>
</div>
<div class="field-group">
<div>
<input type="checkbox" name="remember" id="remember"
<?php if(isset($_COOKIE["member_login"])) { ?> checked
<?php } ?> /> <label for="remember-me">Remember me</label>
</div>
</div>
<div class="field-group">
<div>
<input type="submit" name="login" value="Login"
class="form-submit-button"></span>
</div>
</div>
</form>
A PHP page authCookieSessionValidate.php contains the session and cookie-based logged-in state validation code. It is included at the beginning of the application pages for which the user needs to be authenticated.
If the logged-in state exists with the session or cookie array, then this code will set $loggedIn flag to true. Based on this boolean value, the user will be allowed to proceed with the application or redirected back to the login page.
First, the remembered login is checked with the PHP session. If it returns false, then the code will search for the authentication keys stored in the cookies. If the keys are not empty then they will be hashed compared with the database.
Once the match found then the expiration date is validated with the current date and time. Once the code passes through with all the validation, the user will be redirected to the dashboard.
<?php
require_once "Auth.php";
require_once "Util.php";
$auth = new Auth();
$db_handle = new DBController();
$util = new Util();
// Get Current date, time
$current_time = time();
$current_date = date("Y-m-d H:i:s", $current_time);
// Set Cookie expiration for 1 month
$cookie_expiration_time = $current_time + (30 * 24 * 60 * 60); // for 1 month
$isLoggedIn = false;
// Check if loggedin session and redirect if session exists
if (! empty($_SESSION["member_id"])) {
$isLoggedIn = true;
}
// Check if loggedin session exists
else if (! empty($_COOKIE["member_login"]) && ! empty($_COOKIE["random_password"]) && ! empty($_COOKIE["random_selector"])) {
// Initiate auth token verification diirective to false
$isPasswordVerified = false;
$isSelectorVerified = false;
$isExpiryDateVerified = false;
// Get token for username
$userToken = $auth->getTokenByUsername($_COOKIE["member_login"],0);
// Validate random password cookie with database
if (password_verify($_COOKIE["random_password"], $userToken[0]["password_hash"])) {
$isPasswordVerified = true;
}
// Validate random selector cookie with database
if (password_verify($_COOKIE["random_selector"], $userToken[0]["selector_hash"])) {
$isSelectorVerified = true;
}
// check cookie expiration by date
if($userToken[0]["expiry_date"] >= $current_date) {
$isExpiryDateVerified = true;
}
// Redirect if all cookie based validation retuens true
// Else, mark the token as expired and clear cookies
if (!empty($userToken[0]["id"]) && $isPasswordVerified && $isSelectorVerified && $isExpiryDateVerified) {
$isLoggedIn = true;
} else {
if(!empty($userToken[0]["id"])) {
$auth->markAsExpired($userToken[0]["id"]);
}
// clear cookies
$util->clearAuthCookie();
}
}
?>
In the dashboard screen, it contains the welcome text with the logout link. On clicking the logout link, the remembered login state will be unset from the PHP session and cookies.
<?php
session_start();
require "Util.php";
$util = new Util();
//Clear Session
$_SESSION["member_id"] = "";
session_destroy();
// clear cookies
$util->clearAuthCookie();
header("Location: ./");
?>
Import this SQL script to test this example in your local environment. After set up, try login with admin/admin as the username and the password.
--
-- Database: `db_auth`
--
-- --------------------------------------------------------
--
-- Table structure for table `members`
--
CREATE TABLE `members` (
`member_id` int(8) NOT NULL,
`member_name` varchar(255) CHARACTER SET utf8 NOT NULL,
`member_password` varchar(64) NOT NULL,
`member_email` varchar(255) CHARACTER SET utf8 NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
--
-- Dumping data for table `members`
--
INSERT INTO `members` (`member_id`, `member_name`, `member_password`, `member_email`) VALUES
(1, 'admin', '$2a$10$0FHEQ5/cplO3eEKillHvh.y009Wsf4WCKvQHsZntLamTUToIBe.fG', 'user@gmail.com');
-- --------------------------------------------------------
--
-- Table structure for table `tbl_token_auth`
--
CREATE TABLE `tbl_token_auth` (
`id` int(11) NOT NULL,
`username` varchar(255) NOT NULL,
`password_hash` varchar(255) NOT NULL,
`selector_hash` varchar(255) NOT NULL,
`is_expired` int(11) NOT NULL DEFAULT '0',
`expiry_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
--
-- Indexes for dumped tables
--
--
-- Indexes for table `members`
--
ALTER TABLE `members`
ADD PRIMARY KEY (`member_id`);
--
-- Indexes for table `tbl_token_auth`
--
ALTER TABLE `tbl_token_auth`
ADD PRIMARY KEY (`id`);
--
-- AUTO_INCREMENT for dumped tables
--
--
-- AUTO_INCREMENT for table `members`
--
ALTER TABLE `members`
MODIFY `member_id` int(8) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;
--
-- AUTO_INCREMENT for table `tbl_token_auth`
--
ALTER TABLE `tbl_token_auth`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=17;
COMMIT;
These are the classes used to trigger and handle database operations. The database querying is performed efficiently with the MySQLi prepared statement.
Auth.php
<?php
require "DBController.php";
class Auth {
function getMemberByUsername($username) {
$db_handle = new DBController();
$query = "Select * from members where member_name = ?";
$result = $db_handle->runQuery($query, 's', array($username));
return $result;
}
function getTokenByUsername($username,$expired) {
$db_handle = new DBController();
$query = "Select * from tbl_token_auth where username = ? and is_expired = ?";
$result = $db_handle->runQuery($query, 'si', array($username, $expired));
return $result;
}
function markAsExpired($tokenId) {
$db_handle = new DBController();
$query = "UPDATE tbl_token_auth SET is_expired = ? WHERE id = ?";
$expired = 1;
$result = $db_handle->update($query, 'ii', array($expired, $tokenId));
return $result;
}
function insertToken($username, $random_password_hash, $random_selector_hash, $expiry_date) {
$db_handle = new DBController();
$query = "INSERT INTO tbl_token_auth (username, password_hash, selector_hash, expiry_date) values (?, ?, ?,?)";
$result = $db_handle->insert($query, 'ssss', array($username, $random_password_hash, $random_selector_hash, $expiry_date));
return $result;
}
function update($query) {
mysqli_query($this->conn,$query);
}
}
?>
DBController.php
<?php
class DBController {
private $host = "localhost";
private $user = "root";
private $password = "test";
private $database = "db_auth";
private $conn;
function __construct() {
$this->conn = $this->connectDB();
}
function connectDB() {
$conn = mysqli_connect($this->host,$this->user,$this->password,$this->database);
return $conn;
}
function runBaseQuery($query) {
$result = mysqli_query($this->conn,$query);
while($row=mysqli_fetch_assoc($result)) {
$resultset[] = $row;
}
if(!empty($resultset))
return $resultset;
}
function runQuery($query, $param_type, $param_value_array) {
$sql = $this->conn->prepare($query);
$this->bindQueryParams($sql, $param_type, $param_value_array);
$sql->execute();
$result = $sql->get_result();
if ($result->num_rows > 0) {
while($row = $result->fetch_assoc()) {
$resultset[] = $row;
}
}
if(!empty($resultset)) {
return $resultset;
}
}
function bindQueryParams($sql, $param_type, $param_value_array) {
$param_value_reference[] = & $param_type;
for($i=0; $i<count($param_value_array); $i++) {
$param_value_reference[] = & $param_value_array[$i];
}
call_user_func_array(array(
$sql,
'bind_param'
), $param_value_reference);
}
function insert($query, $param_type, $param_value_array) {
$sql = $this->conn->prepare($query);
$this->bindQueryParams($sql, $param_type, $param_value_array);
$sql->execute();
}
function update($query, $param_type, $param_value_array) {
$sql = $this->conn->prepare($query);
$this->bindQueryParams($sql, $param_type, $param_value_array);
$sql->execute();
}
}
?>
Thanks for good job. :-)
Welcome Thilan.
thanks a lot
Welcome Mohamed.
Hi!
What if someone stole the cookies of the user?
He will be able to access the user’s account simply right?
so what is the solution of that?
Thanks!
Stealing cookies are far fetched. If there is a fear like that, then we have to forget about the functionality “remember me”.
Awesome Tutorial This Help Me So Much
Welcome Beloved.
Hi :) Everything except one thing is working nicely. It’s not saving any data in the tbl_token_auth table for some reason.
It’s not displaying in php errors(I have php errors on for testing at this stage). I am using MariaDB with php version 7.2. I know it is something in the Auth.php file that need to be done, but I am not sure what to change. Any ideas what might be wrong?
Chris,
It is working in my environment and many of the users are using it. If you can share me details with errors, exception, some sort of debug information, then we can zero down the issue. You need to debug and get to the error.
I found the issue in my WAMP setup and it is working since I fixed the settings. Thank you so much! Kudos to the Master :)
Welcome Chris. Glad you figured out.
Hello, thank you so much for your kind explanation. I have this small questions. Would like to have your feedback.
I am thinking to get users mac address and create some hash and then store that hash as a cookie. Is this good?
Definitely not. What is the intention to store it in the cookie? If you can explain the business case, I can suggest you better alternatives.
Thanks alot
Welcome Richard.
Very well explained!
Thank you so much!
Welcome.
how to create signup for this article?
Hi Nishant,
Use this for signup creation and combine these two codes.
I Would really like this script with PDO
Hi Bjorn,
I will try to post it too.
thanks for sharing your knowledge
Welcome Cbla.
thanks it helped me a lot
Welcome Abdel.
Wow!! Congratulations on the explanation and the example, great quality, clarity and objectivity.
Served me like a glove! Can I use your code in my project?
One question: why did you create a ‘password’ and a ‘selector’ in the cookie? Wouldn’t just a password be enough? Is it to increase security or does it have a more specific purpose even if I didn’t understand?
Thank you very much and again, congratulations !!
Thank you Kcscoelho. Yes, go ahead and use the code in your projects. It’s free to use. Yes, I did it to increase the security. Thank you.
Thanks. That’s a big help. Thanx again, Vincy.
Welcome Alex.
Thank you so much!
Welcome Adelmo.
This tutorial helped me a lot
Thank you Dev.
Thanks for your help.
A greetings.
Welcome Carlos.
Hi Vincy – I have a quick question that I was wondering if you wouldn’t mind clarifying for me. I have noticed that you reuse the same basic DBController classes on many of your projects. Within that, you have a method named bindQueryParams. What is the purpose of this method? Would you not be able to accomplish the same functionality by just applying a standard bind_param within the RunQuery method directly? I ask because I cannot tease out exactly what the bindQueryParams is doing, but it seems to serve the same purpose as the core PHP function that it invokes (bind_param).
Thank you for your time, great examples, and excellent work.
Hi Ben,
bindQueryParams binds the argument value to the query. Yes, it calls the “bind_param” function from the core PHP. You can find the details in the DataSource.php available in the download.
Thanks for this!
Welcome Octagon.
really good article.
Thank you.
Thank you, for your precious tutorial..
Welcome Rajat.
thank you vary much
Welcome.
It’s really a useful and clear explanation, thanks.
Thank you Meth.