The best way to implement a "Remember Me" function for login in php

I'm currently working on a remember me functionality for my login page and i know the common way is to use setcookie() then $_COOKIE['<some field here>] and assign a value and check that to login, but is there a better way or a safer way?
33 Replies
ZomaTheMasterOfDisaster
I'm curious if I should store the cookie in the database and compare it to the user cookie in the browser local storage (probably not needed just dumb thinking atm) or try something different
Jochem
Jochem•2mo ago
remember me just sets the expiration on the cookie from "end of browser session" to some other, higher value sometimes it also sets the username in a cookie and fills that in when the login page is displayed, either using $_COOKIE on the server, or using javascript on the client probably using session_set_cookie_params
ZomaTheMasterOfDisaster
so things like this
setcookie('remember_me', $userId, time() + 30 * 24 * 60 * 60, '/');

if (isset($_COOKIE['remember_me'])) {
// Look up the user by the cookie value and log them in
$userId = $_COOKIE['remember_me'];
// Log the user in...
}

// Clear the cookie
setcookie('remember_me', '', time() - 3600, '/');
setcookie('remember_me', $userId, time() + 30 * 24 * 60 * 60, '/');

if (isset($_COOKIE['remember_me'])) {
// Look up the user by the cookie value and log them in
$userId = $_COOKIE['remember_me'];
// Log the user in...
}

// Clear the cookie
setcookie('remember_me', '', time() - 3600, '/');
maybe throw in some encryption on the cookie?
Jochem
Jochem•2mo ago
well, that would be a massive security issue I could just guess a userid and log in so no, not that gimme a sec
ZomaTheMasterOfDisaster
like i do this for csrf
// Start a token
if(!isset($_SESSION['token'])) {
$_SESSION['token'] = md5(uniqid(mt_rand(), true));
}

// check if the user is already logged in.
if(isset($_SESSION['loggedIntoMDSite']) && isset($_SESSION['username'])) {
header("location: userpage.php");
}
// Start a token
if(!isset($_SESSION['token'])) {
$_SESSION['token'] = md5(uniqid(mt_rand(), true));
}

// check if the user is already logged in.
if(isset($_SESSION['loggedIntoMDSite']) && isset($_SESSION['username'])) {
header("location: userpage.php");
}
Jochem
Jochem•2mo ago
I thought I had an example handy, but I don't.
<?php
//check credentials and stuff in the script that receives the POST from your login form
//just before session start
set_session_cookie_params($_POST['rememberme']?30*24*3600:0);
session_start();
//set up your session for a logged in user
?>
<?php
//check credentials and stuff in the script that receives the POST from your login form
//just before session start
set_session_cookie_params($_POST['rememberme']?30*24*3600:0);
session_start();
//set up your session for a logged in user
?>
ZomaTheMasterOfDisaster
maybe create a token for the database to compare to the $_COOKIE?
Jochem
Jochem•2mo ago
you're overthinking this just make sure the session lasts longer when a user check remember me
ZomaTheMasterOfDisaster
that's a one month cookie right?
Jochem
Jochem•2mo ago
yeah
ZomaTheMasterOfDisaster
right now this is what I got
<?php
include('../helpers/validateForms.php');
include('../controller/usercontroller.php');
session_start();

$validator = new Validate;
$userControl = new UserController;
$res = "";

$options = [
'cost' => 12,
];

// Start a token
if(!isset($_SESSION['token'])) {
$_SESSION['token'] = md5(uniqid(mt_rand(), true));
}

// check if the user is already logged in.
if(isset($_SESSION['loggedIntoMDSite']) && isset($_SESSION['username'])) {
header("location: userpage.php");
}

if(isset($_POST['submit'])) {
$errorEmail = $validator::validateEmail($_POST['email']);
$errorPass = $validator::validatePassword($_POST['password']);



if(empty($errorEmail) && empty($errorPass)) {

$email = htmlspecialchars($_POST['email'], ENT_QUOTES, "UTF-8");
$pass = $_POST['password'];

if(!isset($_POST['token']) || $_POST['token'] !== $_SESSION['token']) {
$res = "cannot process login request";
}

$existingUser = $userControl->find_User($email);



if($existingUser == FALSE) {
$res = "User does not exist in the system";
} else {
if(password_verify($pass, $existingUser['password']) == true) {
$res = "Thank you for signing in. Redirecting to your page";
$_SESSION['loggedIntoMDSite'] = true;
$_SESSION['username'] = md5(uniqid(mt_rand(), true));
header("location: userpage.php");
exit;
} else {
$res = "password does not match";
}
}
}
}
<?php
include('../helpers/validateForms.php');
include('../controller/usercontroller.php');
session_start();

$validator = new Validate;
$userControl = new UserController;
$res = "";

$options = [
'cost' => 12,
];

// Start a token
if(!isset($_SESSION['token'])) {
$_SESSION['token'] = md5(uniqid(mt_rand(), true));
}

// check if the user is already logged in.
if(isset($_SESSION['loggedIntoMDSite']) && isset($_SESSION['username'])) {
header("location: userpage.php");
}

if(isset($_POST['submit'])) {
$errorEmail = $validator::validateEmail($_POST['email']);
$errorPass = $validator::validatePassword($_POST['password']);



if(empty($errorEmail) && empty($errorPass)) {

$email = htmlspecialchars($_POST['email'], ENT_QUOTES, "UTF-8");
$pass = $_POST['password'];

if(!isset($_POST['token']) || $_POST['token'] !== $_SESSION['token']) {
$res = "cannot process login request";
}

$existingUser = $userControl->find_User($email);



if($existingUser == FALSE) {
$res = "User does not exist in the system";
} else {
if(password_verify($pass, $existingUser['password']) == true) {
$res = "Thank you for signing in. Redirecting to your page";
$_SESSION['loggedIntoMDSite'] = true;
$_SESSION['username'] = md5(uniqid(mt_rand(), true));
header("location: userpage.php");
exit;
} else {
$res = "password does not match";
}
}
}
}
i start the session very early in the login page
Jochem
Jochem•2mo ago
the alternative would be to set a timestamp in the session and discard the session when it's expired you'd have to check in some bit of global code though
ZomaTheMasterOfDisaster
might go with your first suggestion to not overthink it my goal is once the admin stuff is added and everything is tested ill put it on #showcase for suggestions on ways to make the php code better and security if needed so i can learn new skills where would you put the cooke param code in my above code?
Jochem
Jochem•2mo ago
<?php
include('../helpers/validateForms.php');
include('../controller/usercontroller.php');

$validator = new Validate;
$userControl = new UserController;
$res = "";

$options = [
'cost' => 12,
];

if(isset($_POST['submit'])) {
$errorEmail = $validator::validateEmail($_POST['email']);
$errorPass = $validator::validatePassword($_POST['password']);



if(empty($errorEmail) && empty($errorPass)) {

$email = htmlspecialchars($_POST['email'], ENT_QUOTES, "UTF-8");
$pass = $_POST['password'];

if(!isset($_POST['token']) || $_POST['token'] !== $_SESSION['token']) {
$res = "cannot process login request";
}

$existingUser = $userControl->find_User($email);



if($existingUser == FALSE) {
$res = "User does not exist in the system";
} else {
/***********************************
H E R E
***********************************/
session_bla_cookie_thing();

session_start();

// Start a token
if(!isset($_SESSION['token'])) {
$_SESSION['token'] = md5(uniqid(mt_rand(), true));
}

// check if the user is already logged in.
if(isset($_SESSION['loggedIntoMDSite']) && isset($_SESSION['username'])) {
header("location: userpage.php");
}

if(password_verify($pass, $existingUser['password']) == true) {
$res = "Thank you for signing in. Redirecting to your page";
$_SESSION['loggedIntoMDSite'] = true;
$_SESSION['username'] = md5(uniqid(mt_rand(), true));
header("location: userpage.php");
exit;
} else {
$res = "password does not match";
}
}
}
}
<?php
include('../helpers/validateForms.php');
include('../controller/usercontroller.php');

$validator = new Validate;
$userControl = new UserController;
$res = "";

$options = [
'cost' => 12,
];

if(isset($_POST['submit'])) {
$errorEmail = $validator::validateEmail($_POST['email']);
$errorPass = $validator::validatePassword($_POST['password']);



if(empty($errorEmail) && empty($errorPass)) {

$email = htmlspecialchars($_POST['email'], ENT_QUOTES, "UTF-8");
$pass = $_POST['password'];

if(!isset($_POST['token']) || $_POST['token'] !== $_SESSION['token']) {
$res = "cannot process login request";
}

$existingUser = $userControl->find_User($email);



if($existingUser == FALSE) {
$res = "User does not exist in the system";
} else {
/***********************************
H E R E
***********************************/
session_bla_cookie_thing();

session_start();

// Start a token
if(!isset($_SESSION['token'])) {
$_SESSION['token'] = md5(uniqid(mt_rand(), true));
}

// check if the user is already logged in.
if(isset($_SESSION['loggedIntoMDSite']) && isset($_SESSION['username'])) {
header("location: userpage.php");
}

if(password_verify($pass, $existingUser['password']) == true) {
$res = "Thank you for signing in. Redirecting to your page";
$_SESSION['loggedIntoMDSite'] = true;
$_SESSION['username'] = md5(uniqid(mt_rand(), true));
header("location: userpage.php");
exit;
} else {
$res = "password does not match";
}
}
}
}
ZomaTheMasterOfDisaster
gonna give it a shot and see what happens!
Jochem
Jochem•2mo ago
hm, I see that would be a problem for the CSRF stuff though, I missed that $_SESSION reference
ZomaTheMasterOfDisaster
yeah wasn't sure about the password verify being moved where it is
Jochem
Jochem•2mo ago
I never really bothered with CSRF for login forms, but it's also complex enough I probably wouldn't roll my own
ZomaTheMasterOfDisaster
yeah it does have its uses though like prg without double form submission and preventing bad actors to an extent
Jochem
Jochem•2mo ago
double form submission on a login form shouldn't matter
ZomaTheMasterOfDisaster
really? Just registering?
Jochem
Jochem•2mo ago
I mean, I guess someone might be able to replay the request if they got ahold of it? But then wouldn't they already have the response anyway?
ZomaTheMasterOfDisaster
yeah, i noticed you moved the token generation into the submit is it only recommend to do that during submit?
Jochem
Jochem•2mo ago
I wasn't paying that much attention tbh
ZomaTheMasterOfDisaster
oh ok
Jochem
Jochem•2mo ago
like I said, I don't have experience rolling my own CSRF
ZomaTheMasterOfDisaster
for a future project what's a good way to handle csrf?
Jochem
Jochem•2mo ago
dunno, none of the business software I wrote had CSRF 🤣
ZomaTheMasterOfDisaster
wild 😄
Jochem
Jochem•2mo ago
I think you might be able to use a second named session, one for login and one for CSRF stuff? anyway, I gotta go do some other stuff
ZomaTheMasterOfDisaster
ty for your help 🙂 will update if any cookie issues persist ran into an issue where my database isn't updating the change for the cookie and cookieexpire fields
ZomaTheMasterOfDisaster
these are the new functions in my User model to help process the change

public function addUserCookie($id, $cookie, $expire) {
$db = new DB();
$conn = $db->connect();
if($conn == null) {
echo "connection has died";
}
$sql = "UPDATE Users SET cookie=?, cookieexpire=? WHERE user_id=?";
$stmt = $conn->prepare($sql);
$res = $stmt->execute([$cookie, $expire, $id]);
return $res;
}

public function findUserByCookie($cookie) {
$db = new DB();
$conn = $db->connect();
if($conn == null) {
echo "connection has died";
}
$sql = "SELECT user_id, email FROM Users WHERE cookie=:cookie";
$stmt = $conn->prepare($sql);
$stmt->bindParam(':cookie', $cookie, PDO::PARAM_STR);
$stmt->execute();

return $stmt->fetch(PDO::FETCH_ASSOC);
}

public function addUserCookie($id, $cookie, $expire) {
$db = new DB();
$conn = $db->connect();
if($conn == null) {
echo "connection has died";
}
$sql = "UPDATE Users SET cookie=?, cookieexpire=? WHERE user_id=?";
$stmt = $conn->prepare($sql);
$res = $stmt->execute([$cookie, $expire, $id]);
return $res;
}

public function findUserByCookie($cookie) {
$db = new DB();
$conn = $db->connect();
if($conn == null) {
echo "connection has died";
}
$sql = "SELECT user_id, email FROM Users WHERE cookie=:cookie";
$stmt = $conn->prepare($sql);
$stmt->bindParam(':cookie', $cookie, PDO::PARAM_STR);
$stmt->execute();

return $stmt->fetch(PDO::FETCH_ASSOC);
}
them in the controller
public function add_User_Cookie($id, $cookie, $expire) {
return $this->addUserCookie($id, $cookie, $expire);
}

public function find_User_By_Cookie($cookie) {
return $this->findUserByCookie($cookie);
}
public function add_User_Cookie($id, $cookie, $expire) {
return $this->addUserCookie($id, $cookie, $expire);
}

public function find_User_By_Cookie($cookie) {
return $this->findUserByCookie($cookie);
}
i think the query has to be wrong somehow All I know is the database isn't updating so the new model commands are wrong or I'm not doing it correctly in the login code