Phase 2 · Web FundamentalsModule 13~52 min read

Authentication Foundations

Create safe registration, login, logout, authorization, password, session, and CSRF flows.

What you'll learn

Authentication answers “Who are you?” Authorization answers “May you do this?” You'll build the foundations of both without handling passwords or sessions as ordinary data.

  • Model registration, login, identity, and logout
  • Hash and verify passwords with PHP's password API
  • Regenerate sessions after authentication
  • Check authorization at every protected operation
  • Create and verify CSRF tokens
  • Reduce enumeration, brute-force, and session risks

Separate authentication from authorization

QuestionConcernExample
Who is making this request?AuthenticationA verified session maps to user 42
May this user view the record?AuthorizationUser 42 owns the draft
May this user perform the action?AuthorizationOnly editors may publish
Is this browser request intentional?CSRF defenseThe form carries the session token

Key idea

Being logged in is not permission to access every record. Perform authorization as close as possible to each protected read or write.

Let the password API manage hashes

Store a one-way adaptive hash, never a plaintext password or reversible encrypted copy. PHP's password API includes the algorithm parameters inside the resulting hash.

register-password.php
<?php

declare(strict_types=1);

$password = (string) ($_POST['password'] ?? '');

if (mb_strlen($password) < 12) {
    throw new InvalidArgumentException('Use at least 12 characters.');
}

$hash = password_hash($password, PASSWORD_DEFAULT);

// Store $hash in a column large enough for future algorithm changes.
echo password_verify($password, $hash) ? 'Verified' : 'Rejected';

Never invent password cryptography

Do not use MD5, SHA-1, a plain fast hash, or a custom salt scheme. Use password_hash(), password_verify(), and password_needs_rehash().
rehash.php
<?php

if (password_verify($password, $user['password_hash'])) {
    if (password_needs_rehash($user['password_hash'], PASSWORD_DEFAULT)) {
        $newHash = password_hash($password, PASSWORD_DEFAULT);
        updatePasswordHash($user['id'], $newHash);
    }
}

Create a deliberate login flow

login.php
<?php

declare(strict_types=1);

$email = is_string($_POST['email'] ?? null)
    ? strtolower(trim($_POST['email']))
    : '';
$password = is_string($_POST['password'] ?? null)
    ? $_POST['password']
    : '';

// Replace with a repository lookup by normalized email.
$user = findUserByEmail($email);

$valid = $user !== null
    && password_verify($password, $user['password_hash']);

if (!$valid) {
    http_response_code(422);
    $error = 'The email or password is incorrect.';
} else {
    session_regenerate_id(true);
    $_SESSION['user_id'] = $user['id'];
    header('Location: /dashboard', true, 303);
    exit;
}

Return one generic error whether the account is missing or the password is wrong. Record attempts and apply throttling by multiple signals; do not reveal which addresses are registered.

Note

A real system should verify email ownership, protect recovery tokens, support secure password reset, and consider multi-factor authentication. Identity providers can reduce the amount of sensitive authentication code you own.

Authorize the resource and action

edit-course.php
<?php

declare(strict_types=1);

$userId = $_SESSION['user_id'] ?? null;
$course = findCourseById((int) ($_GET['id'] ?? 0));

if (!is_int($userId)) {
    header('Location: /login', true, 303);
    exit;
}

if ($course === null) {
    http_response_code(404);
    exit('Course not found.');
}

$canEdit = $course['owner_id'] === $userId || userHasRole($userId, 'editor');

if (!$canEdit) {
    http_response_code(403);
    exit('You may not edit this course.');
}

Tip

Centralize recurring rules in policy functions or objects, but invoke them for every protected route. Hiding a button is user experience—not authorization.

Prove a state-changing form came from your app

A CSRF token is random session-bound state embedded in your form and verified on submission. An attacker's site can trigger the browser's cookies but cannot read the token from your page.

csrf.php
<?php

declare(strict_types=1);

function csrfToken(): string
{
    if (!isset($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    }

    return $_SESSION['csrf_token'];
}

function verifyCsrf(?string $submitted): void
{
    $known = $_SESSION['csrf_token'] ?? '';

    if (!is_string($submitted) || !hash_equals($known, $submitted)) {
        http_response_code(403);
        exit('Invalid request token.');
    }
}
delete-form.php
<form method="post" action="/courses/delete.php">
  <input type="hidden" name="csrf_token"
         value="<?= htmlspecialchars(csrfToken(), ENT_QUOTES, 'UTF-8') ?>">
  <input type="hidden" name="course_id" value="42">
  <button>Delete course</button>
</form>

<?php
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
    verifyCsrf($_POST['csrf_token'] ?? null);
    // Authenticate, authorize course 42, then delete it.
}

Logout and layered defenses

  • Accept logout as a CSRF-protected POST, not a GET link
  • Clear session data, expire the identifier cookie, and destroy server state
  • Use HTTPS and secure, HttpOnly, SameSite cookies
  • Apply login throttles without creating a denial-of-service vector
  • Log security events without passwords, tokens, or full session IDs
  • Expire or revoke other sessions when risk warrants it

Watch out

Never put passwords, reset tokens, session identifiers, or CSRF tokens in URLs. URLs leak into browser history, analytics, logs, and referrer headers.

Recap & quick check

Key takeaways

  • Authentication establishes identity; authorization decides access to a specific action and resource.
  • Use PHP's adaptive password API and transparently rehash after successful login when needed.
  • Regenerate the session identifier when authentication changes.
  • Use generic login errors and layered throttling to reduce enumeration and guessing.
  • Every state-changing browser form needs authentication, authorization, validation, and CSRF verification.

Quick check

1. Which function checks a submitted password against its stored hash?

2. When should authorization run?

3. What is the purpose of a CSRF token?

4. Why use a generic login error?