This repository has been archived on 2023-08-20. You can view files and clone it, but cannot push or open issues or pull requests.
indieauth/src/Middleware/DoubleSubmitCookieCsrfMiddl...

129 lines
5.1 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php declare(strict_types=1);
namespace Taproot\IndieAuth\Middleware;
use Nyholm\Psr7\Response;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Dflydev\FigCookies;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use function Taproot\IndieAuth\generateRandomPrintableAsciiString;
/**
* Double-Submit Cookie CSRF Middleware
*
* A PSR-15-compatible Middleware for stateless Double-Submit-Cookie-based CSRF protection.
*
* The `$attribute` property and first constructor argument sets the key by which the CSRF token
* is referred to in all parameter sets (request attributes, request body parameters, cookies).
*
* Generates a random token of length `$tokenLength` (default 128), and stores it as an attribute
* on the `ServerRequestInterface`. Its also added to the response as a cookie.
*
* On requests which may modify state (methods other than HEAD, GET or OPTIONS), the request body
* and request cookies are checked for matching CSRF tokens. If they match, the request is passed on
* to the handler. If they do not match, further processing is halted and an error response generated
* from the `$errorResponse` callback is returned. Refer to the constructor argument for information
* about customising the error response.
*
* @link https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html
* @link https://github.com/zakirullin/csrf-middleware/blob/master/src/CSRF.php
*/
class DoubleSubmitCookieCsrfMiddleware implements MiddlewareInterface, LoggerAwareInterface {
const READ_METHODS = ['HEAD', 'GET', 'OPTIONS'];
const TTL = 60 * 20;
const ATTRIBUTE = 'csrf';
const DEFAULT_ERROR_RESPONSE_STRING = 'Invalid or missing CSRF token!';
const CSRF_TOKEN_LENGTH = 128;
/** @var string $attribute */
public $attribute;
/** @var int $ttl */
public $ttl;
public $errorResponse;
/** @var int $tokenLength */
public $tokenLength;
/** @var LoggerInterface $logger */
public $logger;
/**
* Constructor
*
* The `$errorResponse` parameter can be used to customse the error response returned when a
* write request has invalid CSRF parameters. It can take the following forms:
*
* * A `string`, which will be returned as-is with a 400 Status Code and `Content-type: text/plain` header
* * An instance of `ResponseInterface`, which will be returned as-is
* * A callable with the signature `function (ServerRequestInterface $request): ResponseInterface`,
* the return value of which will be returned as-is.
*/
public function __construct(?string $attribute=self::ATTRIBUTE, ?int $ttl=self::TTL, $errorResponse=self::DEFAULT_ERROR_RESPONSE_STRING, $tokenLength=self::CSRF_TOKEN_LENGTH, $logger=null) {
$this->attribute = $attribute ?? self::ATTRIBUTE;
$this->ttl = $ttl ?? self::TTL;
$this->tokenLength = $tokenLength ?? self::CSRF_TOKEN_LENGTH;
if (!is_callable($errorResponse)) {
if (!$errorResponse instanceof ResponseInterface) {
if (!is_string($errorResponse)) {
$errorResponse = self::DEFAULT_ERROR_RESPONSE_STRING;
}
$errorResponse = new Response(400, ['content-type' => 'text/plain'], $errorResponse);
}
$errorResponse = function (ServerRequestInterface $request) use ($errorResponse) { return $errorResponse; };
}
$this->errorResponse = $errorResponse;
if (!$logger instanceof LoggerInterface) {
$logger = new NullLogger();
}
$this->logger = $logger;
}
public function setLogger(LoggerInterface $logger) {
$this->logger = $logger;
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {
// Generate a new CSRF token, add it to the request attributes, and as a cookie on the response.
$csrfToken = generateRandomPrintableAsciiString($this->tokenLength);
$request = $request->withAttribute($this->attribute, $csrfToken);
if (!in_array(strtoupper($request->getMethod()), self::READ_METHODS) && !$this->isValid($request)) {
// This request is a write method with invalid CSRF parameters.
$response = call_user_func($this->errorResponse, $request);
} else {
$response = $handler->handle($request);
}
// Add the new CSRF cookie, restricting its scope to match the current request.
$response = FigCookies\FigResponseCookies::set($response, FigCookies\SetCookie::create($this->attribute)
->withValue($csrfToken)
->withMaxAge($this->ttl)
->withSecure($request->getUri()->getScheme() == 'https')
->withDomain($request->getUri()->getHost())
->withPath($request->getUri()->getPath()));
return $response;
}
protected function isValid(ServerRequestInterface $request) {
if (array_key_exists($this->attribute, $request->getParsedBody() ?? [])) {
if (array_key_exists($this->attribute, $request->getCookieParams() ?? [])) {
// TODO: make sure CSRF token isnt the empty string, possibly also check that its the same length
// as defined in $this->tokenLength.
return hash_equals($request->getParsedBody()[$this->attribute], $request->getCookieParams()[$this->attribute]);
}
}
return false;
}
}