3.8 KiB
PHP Session Auth Layer — Admin Panel
Addresses: TODO item #2 (No PHP-level authentication in admin panel — 🔴 CRITICAL)
Overview
The admin panel uses two independent authentication layers with a single UX prompt:
| Layer | Mechanism | Configured by |
|---|---|---|
| 1st | nginx HTTP Basic Auth | /etc/nginx/.htpasswd-posterg (see ADMIN_USERS.md) |
| 2nd | PHP session guard (src/AdminAuth.php) |
config/admin_credentials.php |
The user only sees one prompt (the browser Basic Auth dialog). PHP reads the
same password from $_SERVER['PHP_AUTH_PW'] and validates it independently with
password_verify. On success it creates a session so subsequent requests skip
the password_verify call.
Why two layers?
nginx Basic Auth alone is a single point of failure:
- Reverse-proxy misconfiguration could expose admin routes directly.
- Local development without the proxy leaves admin unprotected.
- A misconfigured
auth_basic offblock (e.g., in a nested location) could bypass it.
The PHP session guard (AdminAuth::requireLogin()) is ~100 lines of PHP stdlib
(password_verify + session_regenerate_id) with negligible attack surface.
Authentication flow
Browser → nginx Basic Auth dialog (username + password)
│
▼
nginx validates against .htpasswd ──✗──▶ 401
│ ✓
▼
PHP: AdminAuth::requireLogin()
├─ session already live? ──✓──▶ proceed
├─ $_SERVER['PHP_AUTH_PW'] set?
│ └─ password_verify(PHP_AUTH_PW, ADMIN_PASSWORD_HASH)
│ ├─ ✓ → create session → proceed (normal path)
│ └─ ✗ → redirect to login form
└─ neither → redirect to login form (proxy bypass)
The login form (/admin/login.php) is a fallback for when the reverse proxy
is absent. In normal production use the user never sees it.
PHP auth setup (production)
-
Generate a bcrypt hash for the admin password:
php -r "echo password_hash('your-secret-password', PASSWORD_DEFAULT);" -
Create
config/admin_credentials.php(outside the webroot, never committed):<?php define('ADMIN_PASSWORD_HASH', '$2y$12$<paste-hash-here>'); -
The
bootstrap.phpauto-loads this file if it exists.
If ADMIN_PASSWORD_HASH is not defined (development / cli-server), the PHP
auth layer is a no-op — nginx Basic Auth remains the sole guard.
Session cookie hardening (TODO item #8)
AdminAuth::startSession() calls session_set_cookie_params() before
session_start(), applying:
| Attribute | Value |
|---|---|
HttpOnly |
true |
SameSite |
Strict |
Secure |
true (disabled on cli-server for dev) |
Path |
/admin |
Lifetime |
0 (session cookie, expires on browser close) |
This replaces all direct session_start() calls in admin PHP files.
Logout
A Déconnexion button is shown in the admin nav when ADMIN_PASSWORD_HASH
is defined. It hits /admin/logout.php which destroys the PHP session.
nginx Basic Auth invalidation requires closing the browser tab / window.
Files changed
| File | Change |
|---|---|
src/AdminAuth.php |
New — auth guard class |
config/admin_credentials.php |
New — credential store (gitignored) |
config/admin_credentials.example.php |
New — example / template |
config/bootstrap.php |
Load credentials on startup |
public/admin/*.php |
Replace session_start() with AdminAuth::requireLogin() |
public/admin/actions/*.php |
Same |
public/admin/login.php |
New — login form |
public/admin/logout.php |
New — logout handler |
public/admin/inc/head.php |
Logout button in nav |