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
| Question | Concern | Example |
|---|---|---|
| Who is making this request? | Authentication | A verified session maps to user 42 |
| May this user view the record? | Authorization | User 42 owns the draft |
| May this user perform the action? | Authorization | Only editors may publish |
| Is this browser request intentional? | CSRF defense | The form carries the session token |
Key idea
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.
<?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
password_hash(), password_verify(), and password_needs_rehash().<?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
<?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
Authorize the resource and action
<?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
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.
<?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.');
}
}<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
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?