mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-06 11:09:18 +02:00
- 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)
122 lines
4.0 KiB
PHP
122 lines
4.0 KiB
PHP
<?php
|
|
/**
|
|
* Minimal PHP session guard for the admin panel.
|
|
*
|
|
* This is a defence-in-depth layer that sits behind nginx Basic Auth.
|
|
* It protects against proxy misconfiguration, bypass, and local-dev
|
|
* scenarios where the reverse proxy may be absent.
|
|
*
|
|
* Usage (top of every admin page):
|
|
* require_once __DIR__ . '/../../lib/AdminAuth.php';
|
|
* AdminAuth::requireLogin();
|
|
*
|
|
* Credential setup (production):
|
|
* php -r "echo password_hash('your-password', PASSWORD_DEFAULT);"
|
|
* # Paste result into config/admin_credentials.php as ADMIN_PASSWORD_HASH
|
|
*
|
|
* If ADMIN_PASSWORD_HASH is not defined the guard is a no-op (dev / cli-server).
|
|
*/
|
|
class AdminAuth
|
|
{
|
|
private const SESSION_KEY = 'admin_authenticated';
|
|
private const LOGIN_URL = '/admin/login.php';
|
|
|
|
/**
|
|
* Start the PHP session with hardened cookie parameters.
|
|
* Idempotent — safe to call even if session is already active.
|
|
*/
|
|
private static function startSession(): void
|
|
{
|
|
if (session_status() !== PHP_SESSION_NONE) {
|
|
return;
|
|
}
|
|
// Harden session cookie (item #8)
|
|
session_set_cookie_params([
|
|
'lifetime' => 0,
|
|
'path' => '/admin',
|
|
'secure' => (php_sapi_name() !== 'cli-server'),
|
|
'httponly' => true,
|
|
'samesite' => 'Strict',
|
|
]);
|
|
session_start();
|
|
}
|
|
|
|
/**
|
|
* Gate every admin page.
|
|
*
|
|
* Authentication order:
|
|
* 1. Session already authenticated → pass through.
|
|
* 2. nginx Basic Auth password present in $_SERVER['PHP_AUTH_PW']
|
|
* → validate it with password_verify; on success create session
|
|
* (seamless: user only sees the browser Basic Auth dialog).
|
|
* 3. Neither → redirect to the PHP login form (fallback for when
|
|
* the reverse proxy is absent / misconfigured).
|
|
*
|
|
* No-op if ADMIN_PASSWORD_HASH is not defined (development / cli-server).
|
|
*/
|
|
public static function requireLogin(): void
|
|
{
|
|
self::startSession();
|
|
if (!defined('ADMIN_PASSWORD_HASH')) {
|
|
// No password configured → development / cli-server mode, skip PHP auth.
|
|
return;
|
|
}
|
|
if (!empty($_SESSION[self::SESSION_KEY])) {
|
|
return; // already authenticated via session
|
|
}
|
|
// Try to auto-authenticate from the nginx Basic Auth credentials.
|
|
// If nginx Basic Auth is bypassed, PHP_AUTH_PW won't be set and this
|
|
// branch is skipped — the fallback login form is shown instead.
|
|
if (isset($_SERVER['PHP_AUTH_PW']) && self::login($_SERVER['PHP_AUTH_PW'])) {
|
|
return;
|
|
}
|
|
header('Location: ' . self::LOGIN_URL);
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Validate a plaintext password against the stored bcrypt hash.
|
|
* On success: regenerates the session ID and marks the session authenticated.
|
|
*
|
|
* @return bool true on success, false on wrong password / no hash configured.
|
|
*/
|
|
public static function login(string $password): bool
|
|
{
|
|
$hash = defined('ADMIN_PASSWORD_HASH') ? ADMIN_PASSWORD_HASH : null;
|
|
if ($hash === null || !password_verify($password, $hash)) {
|
|
return false;
|
|
}
|
|
self::startSession();
|
|
session_regenerate_id(true);
|
|
$_SESSION[self::SESSION_KEY] = true;
|
|
$_SESSION['admin_login_at'] = time();
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Check whether the current request is authenticated (without redirecting).
|
|
*/
|
|
public static function isAuthenticated(): bool
|
|
{
|
|
self::startSession();
|
|
return !empty($_SESSION[self::SESSION_KEY]);
|
|
}
|
|
|
|
/**
|
|
* Destroy the session (logout).
|
|
*/
|
|
public static function logout(): void
|
|
{
|
|
self::startSession();
|
|
$_SESSION = [];
|
|
if (ini_get('session.use_cookies')) {
|
|
$p = session_get_cookie_params();
|
|
setcookie(
|
|
session_name(), '', time() - 86400,
|
|
$p['path'], $p['domain'], $p['secure'], $p['httponly']
|
|
);
|
|
}
|
|
session_destroy();
|
|
}
|
|
}
|