# 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
   <?php
   define('ADMIN_PASSWORD_HASH', '$2y$12$<paste-hash-here>');
   ```

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 |
