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
MD
MDOP•7mo ago
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•7mo 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
MD
MDOP•7mo ago
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•7mo 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
MD
MDOP•7mo ago
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•7mo 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
?>
MD
MDOP•7mo ago
maybe create a token for the database to compare to the $_COOKIE?
Jochem
Jochem•7mo ago
you're overthinking this just make sure the session lasts longer when a user check remember me
MD
MDOP•7mo ago
that's a one month cookie right?
Jochem
Jochem•7mo ago
yeah
MD
MDOP•7mo ago
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•7mo 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
MD
MDOP•7mo ago
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•7mo 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";
}
}
}
}
MD
MDOP•7mo ago
gonna give it a shot and see what happens!
Jochem
Jochem•7mo ago
hm, I see that would be a problem for the CSRF stuff though, I missed that $_SESSION reference
MD
MDOP•7mo ago
yeah wasn't sure about the password verify being moved where it is
Jochem
Jochem•7mo ago
I never really bothered with CSRF for login forms, but it's also complex enough I probably wouldn't roll my own
MD
MDOP•7mo ago
yeah it does have its uses though like prg without double form submission and preventing bad actors to an extent
Jochem
Jochem•7mo ago
double form submission on a login form shouldn't matter
MD
MDOP•7mo ago
really? Just registering?
Jochem
Jochem•7mo 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?
MD
MDOP•7mo ago
yeah, i noticed you moved the token generation into the submit is it only recommend to do that during submit?
Jochem
Jochem•7mo ago
I wasn't paying that much attention tbh
MD
MDOP•7mo ago
oh ok
Jochem
Jochem•7mo ago
like I said, I don't have experience rolling my own CSRF
MD
MDOP•7mo ago
for a future project what's a good way to handle csrf?
Jochem
Jochem•7mo ago
dunno, none of the business software I wrote had CSRF 🤣
MD
MDOP•7mo ago
wild 😄
Jochem
Jochem•7mo 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
MD
MDOP•7mo ago
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
MD
MDOP•7mo ago
MD
MDOP•7mo ago
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
Want results from more Discord servers?
Add your server