diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/Http/Response.php | 12 | ||||
-rw-r--r-- | src/Http/SessionServiceProvider.php | 7 | ||||
-rw-r--r-- | src/Middleware/VerifyCsrfToken.php | 90 | ||||
-rw-r--r-- | src/Renderer/Twig/Extensions/Csrf.php | 48 | ||||
-rw-r--r-- | src/Renderer/TwigServiceProvider.php | 4 |
5 files changed, 158 insertions, 3 deletions
diff --git a/src/Http/Response.php b/src/Http/Response.php index 4edf644a..58cd7662 100644 --- a/src/Http/Response.php +++ b/src/Http/Response.php @@ -96,7 +96,7 @@ class Response extends SymfonyResponse implements ResponseInterface /** * Return an instance with the rendered content. * - * THis method retains the immutability of the message and returns + * This method retains the immutability of the message and returns * an instance with the updated status and headers * * @param string $view @@ -111,6 +111,14 @@ class Response extends SymfonyResponse implements ResponseInterface throw new \InvalidArgumentException('Renderer not defined'); } - return $this->create($this->view->render($view, $data), $status, $headers); + $new = clone $this; + $new->setContent($this->view->render($view, $data)); + $new->setStatusCode($status, ($status == $this->getStatusCode() ? $this->statusText : null)); + + foreach ($headers as $key => $values) { + $new = $new->withAddedHeader($key, $values); + } + + return $new; } } diff --git a/src/Http/SessionServiceProvider.php b/src/Http/SessionServiceProvider.php index c2e09624..4d779aa6 100644 --- a/src/Http/SessionServiceProvider.php +++ b/src/Http/SessionServiceProvider.php @@ -5,7 +5,9 @@ namespace Engelsystem\Http; use Engelsystem\Config\Config; use Engelsystem\Container\ServiceProvider; use Engelsystem\Http\SessionHandlers\DatabaseHandler; +use Illuminate\Support\Str; use Symfony\Component\HttpFoundation\Session\Session; +use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage; use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageInterface; @@ -21,6 +23,11 @@ class SessionServiceProvider extends ServiceProvider $session = $this->app->make(Session::class); $this->app->instance(Session::class, $session); $this->app->instance('session', $session); + $this->app->bind(SessionInterface::class, Session::class); + + if (!$session->has('_token')) { + $session->set('_token', Str::random(42)); + } /** @var Request $request */ $request = $this->app->get('request'); diff --git a/src/Middleware/VerifyCsrfToken.php b/src/Middleware/VerifyCsrfToken.php new file mode 100644 index 00000000..cc0c1fbc --- /dev/null +++ b/src/Middleware/VerifyCsrfToken.php @@ -0,0 +1,90 @@ +<?php + +namespace Engelsystem\Middleware; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Server\RequestHandlerInterface; +use Symfony\Component\HttpFoundation\Session\SessionInterface; + +class VerifyCsrfToken implements MiddlewareInterface +{ + /** @var SessionInterface */ + protected $session; + + /** + * @param SessionInterface $session + */ + public function __construct(SessionInterface $session) + { + $this->session = $session; + } + + /** + * Verify csrf tokens + * + * @param ServerRequestInterface $request + * @param RequestHandlerInterface $handler + * @return ResponseInterface + */ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + if ( + $this->isReading($request) + || $this->tokensMatch($request) + ) { + return $handler->handle($request); + } + + return $this->notAuthorizedResponse(); + } + + /** + * @param ServerRequestInterface $request + * @return bool + */ + protected function isReading(ServerRequestInterface $request): bool + { + return in_array( + $request->getMethod(), + ['GET', 'HEAD', 'OPTIONS'] + ); + } + + /** + * @param ServerRequestInterface $request + * @return bool + */ + protected function tokensMatch(ServerRequestInterface $request): bool + { + $token = null; + $body = $request->getParsedBody(); + $header = $request->getHeader('X-CSRF-TOKEN'); + + if (is_array($body) && isset($body['_token'])) { + $token = $body['_token']; + } + + if (!empty($header)) { + $header = array_shift($header); + } + + $token = $token ?: $header; + $sessionToken = $this->session->get('_token'); + + return is_string($token) + && is_string($sessionToken) + && hash_equals($sessionToken, $token); + } + + /** + * @return ResponseInterface + * @codeCoverageIgnore + */ + protected function notAuthorizedResponse(): ResponseInterface + { + // The 419 code is used as "Page Expired" to differentiate from a 401 (not authorized) + return response()->withStatus(419, 'Authentication Token Mismatch'); + } +} diff --git a/src/Renderer/Twig/Extensions/Csrf.php b/src/Renderer/Twig/Extensions/Csrf.php new file mode 100644 index 00000000..9f77df80 --- /dev/null +++ b/src/Renderer/Twig/Extensions/Csrf.php @@ -0,0 +1,48 @@ +<?php + +namespace Engelsystem\Renderer\Twig\Extensions; + +use Symfony\Component\HttpFoundation\Session\SessionInterface; +use Twig_Extension as TwigExtension; +use Twig_Function as TwigFunction; + +class Csrf extends TwigExtension +{ + /** @var SessionInterface */ + protected $session; + + /** + * @param SessionInterface $session + */ + public function __construct(SessionInterface $session) + { + $this->session = $session; + } + + /** + * @return TwigFunction[] + */ + public function getFunctions() + { + return [ + new TwigFunction('csrf', [$this, 'getCsrfField'], ['is_safe' => ['html']]), + new TwigFunction('csrf_token', [$this, 'getCsrfToken']), + ]; + } + + /** + * @return string + */ + public function getCsrfField() + { + return sprintf('<input type="hidden" name="_token" value="%s">', $this->getCsrfToken()); + } + + /** + * @return string + */ + public function getCsrfToken() + { + return $this->session->get('_token'); + } +} diff --git a/src/Renderer/TwigServiceProvider.php b/src/Renderer/TwigServiceProvider.php index 49a0eb90..57ebe9e5 100644 --- a/src/Renderer/TwigServiceProvider.php +++ b/src/Renderer/TwigServiceProvider.php @@ -4,9 +4,10 @@ namespace Engelsystem\Renderer; use Engelsystem\Config\Config as EngelsystemConfig; use Engelsystem\Container\ServiceProvider; -use Engelsystem\Renderer\Twig\Extensions\Authentication; use Engelsystem\Renderer\Twig\Extensions\Assets; +use Engelsystem\Renderer\Twig\Extensions\Authentication; use Engelsystem\Renderer\Twig\Extensions\Config; +use Engelsystem\Renderer\Twig\Extensions\Csrf; use Engelsystem\Renderer\Twig\Extensions\Globals; use Engelsystem\Renderer\Twig\Extensions\Legacy; use Engelsystem\Renderer\Twig\Extensions\Session; @@ -23,6 +24,7 @@ class TwigServiceProvider extends ServiceProvider 'assets' => Assets::class, 'authentication' => Authentication::class, 'config' => Config::class, + 'csrf' => Csrf::class, 'globals' => Globals::class, 'session' => Session::class, 'legacy' => Legacy::class, |