# 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-xamxam` (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 off` block (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) 1. Generate a bcrypt hash for the admin password: ```bash php -r "echo password_hash('your-secret-password', PASSWORD_DEFAULT);" ``` 2. Create `config/admin_credentials.php` (outside the webroot, never committed): ```php '); ``` 3. The `bootstrap.php` auto-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 |