Sessions, Authentication, and Authorization
Authentication proves who a user is. Authorization decides what that user may do. Sessions connect authenticated identity to later HTTP requests.
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 require HTTPS. Regenerate the session identifier after login and privilege changes.
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();
Use the same public error for unknown accounts, wrong passwords, and inactive accounts. Rate-limit login attempts and consider multi-factor authentication for privileged users.
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 permission at the action boundary. Hiding the refund button improves the interface but does not enforce security.
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.