Creating a secure user login and registration system using PHP sessions

Authentication answers two questions: who is this visitor, and should they see protected content? PHP sessions store a server-side session identifier in a cookie while keeping sensitive data on the server. Combined with password hashing, you avoid storing plaintext passwords in MySQL.

Users table design

Store a unique email and a password_hash column wide enough for bcrypt/argon output. Never store reversible passwords or unsalted MD5/SHA1 digests for login systems.

CREATE TABLE users (
          id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
          email VARCHAR(190) NOT NULL UNIQUE,
          password_hash VARCHAR(255) NOT NULL,
          created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        );

Below is how the output typically looks in a browser, terminal, or API client:

Output (MySQL result)
idname
1Sample row
2Another row

2 rows in set

Registration flow

On registration, validate email format, require a minimum password length, hash with password_hash() using PASSWORD_DEFAULT, and insert with a prepared statement. If email already exists, show a generic error to avoid account enumeration.

<?php
        declare(strict_types=1);
        require __DIR__ . '/config.php';

        $email = trim($_POST['email'] ?? '');
        $password = $_POST['password'] ?? '';

        if (!filter_var($email, FILTER_VALIDATE_EMAIL) || strlen($password) < 10) {
            exit('Invalid registration data.');
        }

        $hash = password_hash($password, PASSWORD_DEFAULT);
        $stmt = $pdo->prepare('INSERT INTO users (email, password_hash) VALUES (:e, :h)');
        try {
            $stmt->execute(['e' => $email, 'h' => $hash]);
        } catch (PDOException $e) {
            exit('Could not create account.');
        }

Below is how the output typically looks in a browser, terminal, or API client:

Output (server response)
Could not create account.

Requiring ten or more characters is a baseline policy; add complexity rules only if your audience can tolerate them. Always transmit registration over HTTPS in production so passwords are not readable on the network.

Login and session start

Fetch the user row by email. If no row exists, use the same error message as a wrong password to reduce user enumeration. Call password_verify() to compare the submitted password with the stored hash.

session_start([
            'cookie_httponly' => true,
            'cookie_samesite' => 'Lax',
        ]);

        $stmt = $pdo->prepare('SELECT id, password_hash FROM users WHERE email = :e');
        $stmt->execute(['e' => $email]);
        $user = $stmt->fetch();

        if (!$user || !password_verify($password, $user['password_hash'])) {
            exit('Invalid credentials.');
        }

        $_SESSION['user_id'] = (int) $user['id'];
        header('Location: dashboard.php');

Below is how the output typically looks in a browser, terminal, or API client:

Output (server response)
HTTP 302 Redirect
Location: dashboard.php
(After a valid form submit)

cookie_httponly reduces theft via injected JavaScript. Regenerate the session ID after login (session_regenerate_id(true)) to mitigate session fixation attacks—add that line immediately after a successful verify.

Protecting pages

At the top of dashboard.php, include a guard that redirects anonymous visitors to the login form. Centralize this in auth.php so every protected script calls one function.

function require_login(): void {
            session_start();
            if (empty($_SESSION['user_id'])) {
                header('Location: login.php');
                exit;
            }
        }

Below is how the output typically looks in a browser, terminal, or API client:

Output (server response)
HTTP 302 Redirect
Location: login.php
(After a valid form submit)

Logout

Logout should destroy server session data and expire the session cookie. Never rely only on client-side JavaScript to "log out."

session_start();
        $_SESSION = [];
        session_destroy();
        header('Location: login.php');

Below is how the output typically looks in a browser, terminal, or API client:

Output (server response)
HTTP 302 Redirect
Location: login.php
(After a valid form submit)