I have the following login script which works fine. I want to add a cookie so that the visitor doesn’t have to log in subsequently so I added lines 4-10 and set a couple of cookies later on. Unfortunately that part is not working and on checking the cookies are not being set. I have dumped the cookie array both before and after calling the setcookie function and the array is not changed.
Nothing has been output to the browser before I call setcookie, and there are no errors in my PHP or Apache error logs.
Where be I goin’ wrong?
<?php
session_start();
// Check if the user is already logged in with a cookie (line 4)
if (!empty($_COOKIE['loggedin']) && !empty($_COOKIE['ulevel'])) {
$_SESSION['username'] = $_COOKIE['loggedin'];
$_SESSION['ulevel'] = $_COOKIE['ulevel'];
header('Location: home.php');
exit;
}
if (!empty($_POST)) {
// require user to log in
$db = new PDO('sqlite:users.sqlite');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
$login_ok = false;
$_POST = array_map('trim', $_POST);
$query = 'SELECT username, password, level FROM users WHERE username = :username';
$stmt = $db->prepare($query);
$stmt->bindParam('username', $_POST['username'], PDO::PARAM_STR);
$stmt->execute();
$row = $stmt->fetch();
if ($row) {
if (password_verify($_POST['password'], $row['password'])) {
$_SESSION['username'] = $_POST['username'];
$_SESSION['ulevel'] = $row['level'];
// set cookie for 24 hours
#var_dump($_COOKIE);
setcookie('loggedin', $_POST['username'], 86400);
setcookie('ulevel', $row['level'], 86400);
#var_dump($_COOKIE);die;
header('Location: home.php');
exit;
}
}
}
session_destroy();
?>
<!DOCTYPE html>
<html lang="en-GB">
<head>
<title>Log in to Admin Area</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="css/login.css">
</head>
<body>
<div class="login">
<h1>Admin Area</h1>
<h2>please log in</h2>
<?php
if (isset($login_ok) && !$login_ok)
echo '<p class="error">Login failed.</p>', "\n";
?>
<form method="post">
<div class="form-row">
<input type="text" class="field" id="username" name="username" required autofocus>
<label for="username">Username</label>
</div>
<div class="form-row">
<input type="password" class="field" id="password" name="password" required>
<label for="password">Password</label>
<div class="eye">👁<input type="checkbox" id="passwdShow"></div>
</div>
<div>
<input type="submit" name="submit" value="Login">
</div>
</form>
</div>
<script src="js/passwdShow.js"></script>
</body>
</html>
I don’t know if this will help, but this is how I hand my login (It’s just a basic login script for my personal website)->
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Check if the submitted CSRF token matches the one stored in the session
if (hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
// Sanitize the username and password input
$username = strip_tags($_POST['username']);
$password = $_POST['password'];
// Verify the user's credentials
if ($loginRepository->verify_credentials($username, $password)) {
// Generate a secure login token
$token = bin2hex(random_bytes(32));
// Store the login token in the database
$loginRepository->store_token_in_database($_SESSION['user_id'], $token);
// Set a secure cookie with the login token
setcookie('login_token', $token, [
'expires' => strtotime('+6 months'),
'path' => '/',
'domain' => DOMAIN,
'secure' => true,
'httponly' => true,
'samesite' => 'Lax'
]);
// Store the login token in the session
$_SESSION['login_token'] = $token;
// Redirect the user to the dashboard
header('Location: ../dashboard.php');
exit;
} else {
// Display an error message for invalid username or password
$error = 'Invalid username or password';
error_log("Login error: " . $error);
}
} else {
// Display an error message
$error = 'Invalid CSRF token';
error_log("Login error: " . $error);
$error = 'An error occurred. Please try again.';
}
}
It redirects when you have those cookies, which remains true once you’re logged in. So you need to add a condition to that line to verify that currently no user is logged in (eg check that $_SESSION['username'] is empty)
Do note though that this construction is pretty insecure. If I know a username and some userlevel I can just set those cookies myself and your site will accept them on face value.
An approach that is normally taken is:
when somebody logs in, generate a random string of about 40 characters or so
store that string, along with the userid in the database
send the string in a cookie to the browser
when nobody is logged in, check for the cookie
of the cookie is there, fetch the user that was linked to it (in the database, as written in step 2)
use the fetched details to populate the session
This also has the advantage that people don’t stay “stuck” in their user role, as it is retrieved every time. This prevents people keeping to many privileges even though they were revoked.
Make sure the string is cryptographically secure, e. g. use bin2hex(random_bytes(20)).
TL;DR most random number generators (such as rand) aren’t truly random and can therefore be compromised easier. You want something that’s truly unpredictable. Most systems nowadays have a system that generates numbers based on network traffic, key strokes, that sort of thing. Which is totally unpredictable. Or you could connect it to a device that reacts to wind or solar flares. Stuff like that.
I’ve once read of an office that has a wall of lava lamps in their hallway with a camera on them that will be used to generate random numbers in case the systems actually can’t produce them (which essentially never happens).
The only thing is that by storing the token in the user table any user can only be remembered on one device, as logging in to a new device will overwrite the token from the previous device.
If you want to overcome this you’d need a separate table with just the token and user ID and then join it to the user table when querying.
Additionally, I would add some expiry time to the token. Can be far in the future, like a month or maybe even a year. But allowing someone to stay logged in forever seems like a bad idea.