new csrf work flow

This commit is contained in:
2026-01-17 05:53:59 +07:00
parent 6bc7f8503a
commit fd8f022cae
7 changed files with 98 additions and 60 deletions

View File

@@ -13,22 +13,21 @@ use Psr\Log\LoggerInterface;
final readonly class AuthController
{
public function __construct(
private LoginService $loginService,
private LoggerInterface $logger)
private LoggerInterface $logger
)
{
}
public function loginForm(): string
public function loginForm(ServerRequestInterface $request): string
{
$error = $_SESSION['login_error'] ?? null;
unset($_SESSION['login_error']);
$error = $request->getQueryParams()['error'] ?? null;
return View::display(new LoginViewModel(
title: 'Login',
error: $error,
csrf: CsrfMiddleware::generateToken()
csrf: CsrfMiddleware::generateToken($request)
));
}
@@ -55,8 +54,6 @@ final readonly class AuthController
);
if ($authToken !== null) {
session_regenerate_id(true);
return new Response(
302,
[
@@ -69,9 +66,10 @@ final readonly class AuthController
);
}
$_SESSION['login_error'] = 'Invalid credentials';
return new Response(302, ['Location' => '/login']);
return new Response(
302,
['Location' => '/login?error=Invalid']
);
}
public function logout(ServerRequestInterface $request): Response
@@ -82,8 +80,6 @@ final readonly class AuthController
$this->loginService->logout($token);
}
session_destroy();
return new Response(
302,
[

View File

@@ -47,7 +47,7 @@ final readonly class DashboardController
'percent' => $percent,
];
}
} catch (Throwable $exception) {
$this->logger->error($exception->getMessage(), ['exception' => $exception]);
return View::display(new DashboardViewModel(
@@ -62,7 +62,7 @@ final readonly class DashboardController
'folders' => [],
],
],
csrf: CsrfMiddleware::generateToken(),
csrf: CsrfMiddleware::generateToken($request),
));
}
@@ -78,7 +78,7 @@ final readonly class DashboardController
'folders' => $folders,
],
],
csrf: CsrfMiddleware::generateToken(),
csrf: CsrfMiddleware::generateToken($request),
));
}
}

View File

@@ -125,7 +125,7 @@ final readonly class StorageController
public function showFolder(ServerRequestInterface $request, string $folder): string
{
$user = $request->getAttribute('user');
$csrfToken = CsrfMiddleware::generateToken();
$csrfToken = CsrfMiddleware::generateToken($request);
$folderPath = $this->storageService->getDriver()->getUserFolderPath($user->id, $folder);
@@ -327,7 +327,7 @@ final readonly class StorageController
if (!is_array($data)) {
return new Response(400);
}
$folder = $data['folder'] ?? '';
$raw = $data['file_names'] ?? '[]';

View File

@@ -3,19 +3,23 @@ declare(strict_types=1);
namespace Din9xtrCloud\Middlewares;
use JsonException;
use Nyholm\Psr7\Factory\Psr17Factory;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Random\RandomException;
use RuntimeException;
final class CsrfMiddleware implements MiddlewareInterface
{
private const array UNSAFE_METHODS = ['POST', 'PUT', 'PATCH', 'DELETE'];
private const int TTL = 3600;
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
): ResponseInterface
{
$method = $request->getMethod();
$path = $request->getUri()->getPath();
@@ -25,11 +29,15 @@ final class CsrfMiddleware implements MiddlewareInterface
}
if (in_array($method, self::UNSAFE_METHODS, true)) {
$token = $request->getParsedBody()['_csrf'] ?? null;
$token = $_POST['_csrf'] ?? '';
if (!isset($_SESSION['_csrf']) || $token !== $_SESSION['_csrf']) {
return new Psr17Factory()->createResponse(403)
->withBody(new Psr17Factory()->createStream('CSRF validation failed'));
if (!$token || !self::validateToken($token, $request)) {
return new Psr17Factory()
->createResponse(403)
->withBody(
new Psr17Factory()
->createStream('CSRF validation failed')
);
}
}
@@ -38,36 +46,63 @@ final class CsrfMiddleware implements MiddlewareInterface
private function isExcludedPath(string $path): bool
{
if (str_starts_with($path, '/storage/tus')) {
return true;
}
if (str_starts_with($path, '/api/')) {
return true;
}
if (str_starts_with($path, '/webhook/')) {
return true;
}
return false;
return
str_starts_with($path, '/storage/tus') ||
str_starts_with($path, '/api/') ||
str_starts_with($path, '/webhook/');
}
public static function generateToken(): string
public static function generateToken(ServerRequestInterface $request): string
{
if (empty($_SESSION['_csrf']) || $_SESSION['_csrf_expire'] < time()) {
try {
$_SESSION['_csrf'] = bin2hex(random_bytes(32));
$_SESSION['_csrf_expire'] = time() + 3600;
} catch (RandomException $e) {
throw new RuntimeException(
'Failed to generate CSRF token: ' . $e->getMessage(),
0,
$e
);
}
try {
$payload = json_encode([
'ts' => time(),
'ua' => hash('sha256', $request->getHeaderLine('User-Agent')),
], JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
error_log($e->getMessage());
}
return $_SESSION['_csrf'];
$payloadB64 = base64_encode($payload);
$signature = hash_hmac('sha256', $payloadB64, self::key(), true);
return $payloadB64 . '.' . base64_encode($signature);
}
public static function validateToken(
string $token,
ServerRequestInterface $request
): bool
{
[$payloadB64, $sigB64] = explode('.', $token, 2) + [null, null];
if (!$payloadB64 || !$sigB64) {
return false;
}
$expected = hash_hmac('sha256', $payloadB64, self::key(), true);
if (!hash_equals($expected, base64_decode($sigB64))) {
return false;
}
$payload = json_decode(base64_decode($payloadB64), true);
if (!isset($payload['ts'], $payload['ua'])) {
return false;
}
if (time() - $payload['ts'] > self::TTL) {
return false;
}
$uaHash = hash('sha256', $request->getHeaderLine('User-Agent'));
return hash_equals($payload['ua'], $uaHash);
}
private static function key(): string
{
return $_ENV['APP_KEY']
?? throw new RuntimeException('APP_KEY not defined');
}
}