Files
xamxam/nginx/PHP_AUTH_LAYER.md
Théophile Gervreau-Mercier 8613f71112 security: add PHP session auth guard for admin panel (item #2, CRITICAL)
- lib/AdminAuth.php: new class with requireLogin(), login(), logout(),
  isAuthenticated(); starts session with hardened cookie params
  (HttpOnly, SameSite=Strict, Secure, Path=/admin) — also resolves
  item #8 (session cookie hardening)
- requireLogin() auto-authenticates from nginx Basic Auth credentials
  ($_SERVER['PHP_AUTH_PW']) so the user only sees one browser prompt;
  falls back to /admin/login.php if the proxy is absent/misconfigured
- config/admin_credentials.php: gitignored credential store; define
  ADMIN_PASSWORD_HASH with a bcrypt hash to enable PHP auth
- config/admin_credentials.example.php: template for the above
- config/bootstrap.php: auto-loads admin_credentials.php if present
- .gitignore: exclude config/admin_credentials.php
- public/admin/login.php: fallback login form (shown only when nginx
  Basic Auth is bypassed / proxy absent)
- public/admin/logout.php: session destruction + redirect to login
- All 7 admin PHP files: replace session_start() with
  AdminAuth::requireLogin() (defence-in-depth behind nginx Basic Auth)
- public/admin/inc/head.php: Déconnexion button when ADMIN_PASSWORD_HASH
  is defined
- nginx/PHP_AUTH_LAYER.md: documents dual-auth architecture, UX flow,
  and setup instructions
- docs/TODO.SECURITY.md: items #2 and #8 moved to Resolved; priority
  order updated (all CRITICAL done)
2026-02-08 14:22:45 +01:00

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 (lib/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:

    php -r "echo password_hash('your-secret-password', PASSWORD_DEFAULT);"
    
  2. Create config/admin_credentials.php (outside the webroot, never committed):

    <?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.


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
lib/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