Ravindra BagaleCourses & study guides

16. Live Project: Building a Reels App with EC2, S3 and RDS

16.6 Accounts: Register, Login and Logout

Tin chote pages. Passwords password_hash() ne hash hotat, login nantar session_regenerate_id(true), aani logout fakt POST + CSRF ne.

public/register.php

<?php
// public/register.php – create an account (password_hash, prepared statements)
require __DIR__ . '/../src/bootstrap.php';

$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    csrf_check();
    $username = trim((string) ($_POST['username'] ?? ''));
    $password = (string) ($_POST['password'] ?? '');

    if (!preg_match('/^[A-Za-z0-9_]{3,30}$/', $username)) {
        $error = 'Username: 3-30 letters, numbers or _';
    } elseif (strlen($password) < 8) {
        $error = 'Password must be at least 8 characters';
    } else {
        try {
            $stmt = db()->prepare('INSERT INTO users (username, password_hash) VALUES (?, ?)');
            $stmt->execute([$username, password_hash($password, PASSWORD_DEFAULT)]);
            session_regenerate_id(true);
            $_SESSION['uid']   = (int) db()->lastInsertId();
            $_SESSION['uname'] = $username;
            header('Location: /');
            exit;
        } catch (PDOException $ex) {
            if (($ex->errorInfo[1] ?? 0) === 1062) {        // duplicate key on UNIQUE(username)
                $error = 'Username already taken';
            } else {
                error_log('register: ' . $ex->getMessage());
                $error = 'Could not register';
            }
        }
    }
}

page_top('Create account');
if ($error !== '') {
    echo '<p class="err">' . e($error) . '</p>';
}
?>
<form method="post" autocomplete="off">
  <input type="hidden" name="csrf" value="<?= e(csrf_token()) ?>">
  <label>Username <input name="username" required maxlength="30" pattern="[A-Za-z0-9_]{3,30}"></label>
  <label>Password <input name="password" type="password" required minlength="8"></label>
  <button type="submit">Register</button>
</form>
<p>Already have an account? <a href="/login.php">Log in</a></p>
<?php page_bottom();

public/login.php

<?php
// public/login.php – log in with password_verify and a fresh session ID
require __DIR__ . '/../src/bootstrap.php';

$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    csrf_check();
    $username = trim((string) ($_POST['username'] ?? ''));
    $password = (string) ($_POST['password'] ?? '');

    $stmt = db()->prepare('SELECT id, username, password_hash FROM users WHERE username = ?');
    $stmt->execute([$username]);
    $user = $stmt->fetch();

    if ($user && password_verify($password, $user['password_hash'])) {
        session_regenerate_id(true);              // prevents session fixation
        $_SESSION['uid']   = (int) $user['id'];
        $_SESSION['uname'] = $user['username'];
        header('Location: /');
        exit;
    }
    usleep(300000);                               // slow down password guessing a little
    $error = 'Wrong username or password';        // same message for both cases
}

page_top('Log in');
if ($error !== '') {
    echo '<p class="err">' . e($error) . '</p>';
}
?>
<form method="post">
  <input type="hidden" name="csrf" value="<?= e(csrf_token()) ?>">
  <label>Username <input name="username" required maxlength="30"></label>
  <label>Password <input name="password" type="password" required></label>
  <button type="submit">Log in</button>
</form>
<p>New here? <a href="/register.php">Create an account</a></p>
<?php page_bottom();

public/logout.php

<?php
// public/logout.php – POST only, with CSRF check
require __DIR__ . '/../src/bootstrap.php';

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    exit('Method not allowed');
}
csrf_check();
$_SESSION = [];
session_destroy();
setcookie(session_name(), '', ['expires' => time() - 3600, 'path' => '/',
          'secure' => cfg('cookie_secure'), 'httponly' => true, 'samesite' => 'Lax']);
header('Location: /login.php');

Why this matters for security

password_hash() uses a slow, salted algorithm, so a stolen users table cannot be reversed easily. One generic message ("Wrong username or password") stops username enumeration (वापरकर्ता नाव शोधणे). Regenerating the session ID on login defeats session fixation, and a POST-only logout with CSRF stops other sites from logging your users out.

Ravindra Bagale's Tip

When login fails, students show different messages like "User not found" and "Wrong password" – so an attacker learns which usernames exist! Always show a single message. And never use md5() or sha1() for passwords – only password_hash() / password_verify().

Lab

Register two users, log in and out, and look at the users table – confirm that password_hash starts with $2y$. Try logging in with a wrong password and a non-existent user and confirm the message is identical.