Sessions, Authentication, and Authorization
Three things that get mixed up a lot: authentication is proving who you are, authorization is deciding what you're allowed to do, and sessions are how the app remembers you between requests. Get any one wrong and you have a security problem, so this is a page I'm careful with.
Secure session initialization
<?php
declare(strict_types=1);
ini_set('session.use_strict_mode', '1');
ini_set('session.cookie_httponly', '1');
ini_set('session.cookie_secure', '1');
ini_set('session.cookie_samesite', 'Lax');
session_start();
Secure cookies only work over HTTPS, so make sure the whole site is HTTPS. And always regenerate the session ID after login and whenever privileges change. That's what prevents session fixation.
Registration
<?php
declare(strict_types=1);
$email = mb_strtolower(trim($_POST['email'] ?? ''));
$password = $_POST['password'] ?? '';
if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
throw new InvalidArgumentException('Invalid email address.');
}
if (strlen($password) < 12) {
throw new InvalidArgumentException('Use at least 12 characters.');
}
$passwordHash = password_hash($password, PASSWORD_DEFAULT);
$stmt = $pdo->prepare(
'INSERT INTO users (email, password_hash, created_at)
VALUES (:email, :password_hash, UTC_TIMESTAMP())'
);
$stmt->execute([
'email' => $email,
'password_hash' => $passwordHash,
]);
Login
<?php
declare(strict_types=1);
$stmt = $pdo->prepare(
'SELECT id, email, password_hash, is_active
FROM users
WHERE email = :email
LIMIT 1'
);
$stmt->execute(['email' => mb_strtolower(trim($_POST['email'] ?? ''))]);
$user = $stmt->fetch();
$valid = $user
&& (bool) $user['is_active']
&& password_verify($_POST['password'] ?? '', $user['password_hash']);
if (!$valid) {
usleep(random_int(150000, 350000));
throw new RuntimeException('Invalid email or password.');
}
session_regenerate_id(true);
$_SESSION['user_id'] = (int) $user['id'];
$_SESSION['authenticated_at'] = time();
Show the same error for an unknown account, a wrong password, and a disabled account, so attackers can't use your login form to find out which accounts exist. Rate-limit login attempts, and put MFA on anyone with admin powers.
Authorization
<?php
declare(strict_types=1);
function requirePermission(User $user, string $permission): void
{
if (!$user->hasPermission($permission)) {
http_response_code(403);
throw new AuthorizationException('Permission denied.');
}
}
requirePermission($currentUser, 'invoice.refund');
Check permissions where the action actually happens. Hiding the refund button is nice for the interface, but it doesn't stop anyone who sends the request directly.
Password reset design
- Generate a cryptographically random token.
- Store only a hash of the token in the database.
- Set a short expiration time and one-time-use flag.
- Return the same public response whether the email exists or not.
- Invalidate other active sessions after a sensitive password reset.
- Record the event in the audit log.
