PHP Authentication and Sessions: A Security Reference
PHP authentication checks which account is signing in. A session keeps that account signed in between requests, usually by storing a session ID in a browser cookie and the account data on the server.
This guide explains password checks, session cookies, login guards, and logout. The snippets assume a PDO connection, validated email and password strings, and a users table with a unique email and password_hash column. For production account recovery, MFA, or SSO, use a maintained authentication framework or identity provider rather than building every feature yourself.
How Should PHP Store and Check Passwords?
PHP should store passwords with password_hash() and check login attempts with password_verify(). Password hashing makes stolen passwords harder to recover; it does not make a leaked database harmless. Do not store plaintext passwords or use fast general-purpose hashes such as MD5 or SHA-1.
<?php
$hash = password_hash($plainPassword, PASSWORD_DEFAULT);
$statement = $pdo->prepare(
"INSERT INTO users (email, password_hash)
VALUES (:email, :password_hash)",
);
$statement->execute([
"email" => $email,
"password_hash" => $hash,
]); The generated hash includes its algorithm, cost, and salt. A VARCHAR(255) column leaves room for future hash formats. If the selected algorithm is bcrypt, enforce its 72-byte input limit rather than silently truncating longer passwords. After a successful login, use password_needs_rehash() when changing your algorithm or cost.
<?php
$statement = $pdo->prepare(
"SELECT id, email, password_hash
FROM users
WHERE email = :email",
);
$statement->execute(["email" => $email]);
$user = $statement->fetch(PDO::FETCH_ASSOC);
if (! $user || ! password_verify($plainPassword, $user["password_hash"])) {
throw new RuntimeException("Invalid email or password");
} Return the same error for a missing account and a wrong password every single time. The server can log the difference for its own triage, but a browser that learns which email addresses exist has just been handed a free enumeration tool.
How Do You Configure PHP Session Cookies?
Configure PHP session cookies before calling session_start(). Use HTTPS, strict mode, cookie-only session IDs, and Secure, HttpOnly, and SameSite cookie attributes.
<?php
ini_set("session.use_strict_mode", "1");
ini_set("session.use_only_cookies", "1");
session_set_cookie_params([
"lifetime" => 0,
"path" => "/",
"secure" => true,
"httponly" => true,
"samesite" => "Lax",
]);
session_start(); A lifetime of zero creates a session cookie, but browsers may restore it when reopening a previous session. Enforce expiry on the server too. Secure restricts transmission to HTTPS; HttpOnly prevents JavaScript from reading the cookie, but does not stop injected scripts from making requests. SameSite=Lax helps with cross-site requests but does not replace CSRF tokens. Strict mode rejects uninitialized IDs when the session handler supports validation.
When Should PHP Regenerate the Session ID?
Regenerate the PHP session ID after a successful login, before storing the authenticated user ID. Also rotate it when privileges increase. This prevents a known pre-login ID from gaining authenticated access.
<?php
if (! session_regenerate_id()) {
throw new RuntimeException("Could not rotate session");
}
$_SESSION["user_id"] = (int) $user["id"];
$_SESSION["authenticated_at"] = time(); Start the session and verify the password before this snippet. Rotate the ID first, then store the authenticated user and timestamp.
Production session management also needs server-side idle and absolute timeouts, revocation, and handling for concurrent requests. Rotation does not delete old session data by default. Use timestamps to control when old IDs stop working rather than immediately deleting active data during rotation.
Keep the Login Guard Small
An authenticated page should not know how login works. It asks one small guard whether a user ID is present in the session, then gets on with its job.
<?php
function requireUserId(): int
{
if (! isset($_SESSION["user_id"])) {
header("Location: /login", true, 302);
exit;
}
return (int) $_SESSION["user_id"];
} That guard answers exactly one question, which is whether someone is logged in. It is not the authorization system, and treating it as one is how a logged-in user reaches a page they were never cleared for. A user ID in the session does not prove the holder can edit this invoice, delete this post, or open the admin panel. Authentication identifies the account, and a separate permission check decides what that account is allowed to touch.
<?php
$userId = requireUserId();
$statement = $pdo->prepare(
"SELECT id, email
FROM users
WHERE id = :id",
);
$statement->execute(["id" => $userId]); The login guard identifies the account. Queries for private records must also check ownership or another permission rule.
How Do You Track Recently Active PHP Users?
Track recently active PHP users by saving their last request time on the server. A recent request suggests activity; it does not prove someone is still looking at the page. The MySQL example below assumes user_presence.user_id is a primary or unique key and last_seen_at stores UTC timestamps. Set the MySQL connection timezone to UTC with SET time_zone = '+00:00' before using CURRENT_TIMESTAMP, and calculate the PHP cutoff in UTC too.
<?php
function touchPresence(PDO $pdo, int $userId): void
{
$statement = $pdo->prepare(
"INSERT INTO user_presence (user_id, last_seen_at)
VALUES (:user_id, CURRENT_TIMESTAMP)
ON DUPLICATE KEY UPDATE last_seen_at = CURRENT_TIMESTAMP",
);
$statement->execute(["user_id" => $userId]);
} List the active accounts by a short trailing window and let the timestamp do the work.
<?php
$cutoff = (new DateTimeImmutable("-5 minutes", new DateTimeZone("UTC")))->format("Y-m-d H:i:s");
$statement = $pdo->prepare(
"SELECT u.id, u.email, p.last_seen_at
FROM user_presence p
JOIN users u ON u.id = p.user_id
WHERE p.last_seen_at >= :cutoff
ORDER BY p.last_seen_at DESC",
);
$statement->execute(["cutoff" => $cutoff]);
$onlineUsers = $statement->fetchAll(PDO::FETCH_ASSOC); A single database table covers a small app comfortably. A busier chat or community app wants a shared store with expirations, refreshed through a heartbeat request. Either way, keep the claim honest and label it as presence: this account was active recently, not a guarantee that someone is staring at the page right now.
Logout
Log out by clearing the server-side session and expiring its cookie. Run this in a POST handler after starting the session and verifying a CSRF token. Deleting only the browser cookie does not invalidate a copy of the session ID.
<?php
$_SESSION = [];
if (ini_get("session.use_cookies")) {
$params = session_get_cookie_params();
setcookie(
session_name(),
"",
[
"expires" => time() - 3600,
"path" => $params["path"],
"domain" => $params["domain"],
"secure" => $params["secure"],
"httponly" => $params["httponly"],
"samesite" => $params["samesite"] ?? "Lax",
],
);
}
session_destroy(); Don't lean on logout as your only way to end a session. A password change, an account recovery, or a flagged login event may all need to invalidate active sessions that the user never logged out of, and you want that path built before you need it.
Do Logged-In PHP Forms Still Need CSRF Protection?
Logged-in PHP forms still need CSRF protection. Browsers can attach session cookies automatically to requests that meet the cookie’s scope and SameSite rules. Require a valid session token for state-changing forms, including logout.
<?php
function csrfToken(): string
{
if (! isset($_SESSION["csrf_token"])) {
$_SESSION["csrf_token"] = bin2hex(random_bytes(32));
}
return $_SESSION["csrf_token"];
}
function requireCsrfToken(mixed $token): void
{
$expected = $_SESSION["csrf_token"] ?? null;
if (! is_string($expected) || $expected === "" || ! is_string($token)
|| ! hash_equals($expected, $token)) {
throw new RuntimeException("Invalid form token");
}
} Carry the token in the form that performs the action, escaped on the way out.
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(csrfToken(), ENT_QUOTES) ?>"> A valid token proves the request carried a value your server planted in the session, and it says nothing about whether the submitted data is any good. You still validate every field and you still write through prepared statements. The token closes the forgery gap, and the rest of your input handling stays exactly as important as it was.
Does CAPTCHA Make a PHP Login Secure?
CAPTCHA can reduce some automated abuse, but it does not identify a user or secure a PHP login by itself. Use it selectively alongside rate limits, password protections, session management, and accessible recovery options.
Putting CAPTCHA in front of every login by default punishes legitimate users, adds accessibility friction, and still gets bypassed or outsourced to a solving service for pennies. A login flow with safe password storage, measured throttling, session rotation, and reviewable logs is a stronger first line than a challenge that mostly annoys the people you want to keep.
How Should PHP Implement Remember Me?
Implement PHP remember-me login with separate, revocable tokens rather than keeping the active session ID valid indefinitely. Use a maintained authentication library where possible. A typical design has these parts:
- Store a selector alongside a hashed validator in the database.
- Send the raw validator only inside the cookie.
- Rotate the token every time it gets used.
- Expire tokens on a deliberate schedule.
- Let the user revoke a remembered device.
That takes more work than increasing the session lifetime, but keeps persistent-login tokens separate from active sessions if either is compromised.
Frequently Asked Questions
How long should a PHP login session last?
A PHP login session should expire after a defined period of inactivity and a separate maximum lifetime. Choose shorter limits for sensitive accounts. Check timestamps on the server; cookie expiry and garbage collection alone do not enforce an authentication timeout.
Should PHP sessions use a database instead of files?
File-based PHP sessions can suit one server. A database or shared store such as Redis is useful when several servers need the same sessions. Listing and revoking a user’s sessions also requires application logic; changing the storage backend alone does not add it.
What happens when a PHP user logs in from two devices?
A PHP user can have separate login sessions on two devices by default. Each device holds its own session ID. To allow only one active session, the application must track sessions and revoke earlier ones.
Is it safe to store a user ID in a PHP session?
A user ID can be stored in a server-side PHP session after authentication. Protect the session ID and storage, and check permissions separately for each private action. Do not store the user’s password in the session.
Do you need to call session_start() on every page?
Call session_start() before reading or writing PHP session data, unless the session is already active. With cookie-based sessions, start it before sending output. Requests that do not need session data can skip it.
Sources
-
[1]
PHP password_hash(php.net)
-
[2]
PHP session_set_cookie_params(php.net)
-
[3]
PHP session_regenerate_id(php.net)
-
[4]
Securing Session INI Settings(php.net)
-
[5]
OWASP Authentication Cheat Sheet(cheatsheetseries.owasp.org)
-
[6]
OWASP Session Management Cheat Sheet(cheatsheetseries.owasp.org)
-
[7]
OWASP Cross-Site Request Forgery Prevention Cheat Sheet(cheatsheetseries.owasp.org)
-
[8]
OWASP Bot Management and Anti-Automation Cheat Sheet(cheatsheetseries.owasp.org)
-
[9]
PDO::prepare(php.net)
Read Next
Secure ordinary PHP surfaces with current password hashing, sessions, CSRF tokens, output escaping, prepared statements, and upload controls.
Use PDO prepared statements to connect PHP to MySQL safely and keep SQL input handling clear.
A beginner's introduction to PHP: whether it is worth learning in 2026, what runs on it, PHP or Node, whether it is secure, how coding agents change the work, and a roadmap to every PHP guide.