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.