Files
novaconium/novaconium/lib/Csrf.php
T
codeandClaude Fable 5 b882c304b1 Replace Basic Auth with multi-user login, roles, groups, and Lib\Access
Admin login & user management (novaconium/ISSUES.md): session-based
login against a SQLite users table replaces the single-user HTTP Basic
Auth stopgap (admin_username/admin_password_hash and /admin/password-hash
are gone; one admin_auth_enabled flag, off by default with zero DB
footprint). New /admin/login, /admin/logout (POST-only, real page), and
/admin/users pages plus bin/create-admin-user.php.

First user created is the admin; everyone after is registered with a
unique normalized email and an optional group. /admin/* and drafts are
admin-only; Lib\Access gates page content from sidecars
(Access::require('group:members')) with login-redirect/404 responses —
public by default, static pages always public by construction. User
management covers disable/enable, delete, promote/demote, group, email,
and password, with last-active-admin lockout guards.

Also: Session::regenerate() against fixation, friendly missing-PDO-driver
errors in Lib\Db, docs at /admin/docs/access-control and updates across
admin-auth/drafts/sidecars/config/libraries and README/AGENTS.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 00:17:54 +00:00

75 lines
2.1 KiB
PHP

<?php
namespace Lib;
/**
* Session-token CSRF protection, standalone from Lib\FormValidator — a
* sidecar calls Csrf::verify() directly, typically before running any other
* validation:
*
* if (!Csrf::verify(Input::post('csrf_token'))) {
* return Response::redirect('/contact?error=security');
* }
*
* and the template renders a hidden field for it:
*
* <input type="hidden" name="{{ csrfField }}" value="{{ csrfToken }}">
*
* This was the first thing in the framework to start a native PHP session
* — but only lazily, the moment token()/verify() is actually called. A page
* that never touches Csrf never gets a session cookie. Lib\Session and
* App\AdminAuth (session-based admin login) now touch the same native
* session the same lazy way — safe in any order, since ensureSession()
* no-ops when a session is already active.
*/
final class Csrf
{
public const FIELD_NAME = 'csrf_token';
private const SESSION_KEY = '_csrf_token';
public static function token(): string
{
self::ensureSession();
if (empty($_SESSION[self::SESSION_KEY])) {
$_SESSION[self::SESSION_KEY] = bin2hex(random_bytes(32));
}
return $_SESSION[self::SESSION_KEY];
}
public static function verify(?string $submittedToken): bool
{
self::ensureSession();
$expected = $_SESSION[self::SESSION_KEY] ?? null;
if ($submittedToken === null || $expected === null) {
return false;
}
return hash_equals($expected, $submittedToken);
}
public static function fieldName(): string
{
return self::FIELD_NAME;
}
private static function ensureSession(): void
{
if (session_status() === PHP_SESSION_ACTIVE) {
return;
}
// Must be called before session_start() — after is a silent no-op.
session_set_cookie_params([
'httponly' => true,
'samesite' => 'Lax',
'secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off',
]);
session_start();
}
}