diff --git a/CHANGELOG.md b/CHANGELOG.md index cf90a7d..56db039 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ # Yii CSRF Protection Library Change Log -## 1.1.1 under development +## 1.2.0 under development +- Enh #30: Add a custom failure handler feature to `CsrfMiddleware` (solventt, devanych) - Chg #31: Update `yiisoft/http` dependency (devanych) ## 1.1.0 October 21, 2021 diff --git a/README.md b/README.md index 400a7c5..6e9e0a7 100644 --- a/README.md +++ b/README.md @@ -48,8 +48,8 @@ return [ ->withMiddlewares( [ ErrorCatcher::class, - SessionMiddleware::class, // <-- add this - CsrfMiddleware::class, + SessionMiddleware::class, + CsrfMiddleware::class, // <-- add this Router::class, ] ); @@ -64,13 +64,47 @@ By default, CSRF token is obtained from `_csrf` request body parameter or `X-CSR You can access currently valid token as a string using `CsrfTokenInterface`: ```php -/** @var \Yiisoft\Csrf\CsrfTokenInterface $csrfToken */ +/** @var Yiisoft\Csrf\CsrfTokenInterface $csrfToken */ $csrf = $csrfToken->getValue(); ``` +If the token does not pass validation, the response `422 Unprocessable Entity` will be returned. +You can change this behavior by implementing your own request handler: + +```php +use Psr\Http\Message\ResponseFactoryInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\RequestHandlerInterface; +use Yiisoft\Csrf\CsrfMiddleware; + +/** + * @var Psr\Http\Message\ResponseFactoryInterface $responseFactory + * @var Yiisoft\Csrf\CsrfTokenInterface $csrfToken + */ + +$failureHandler = new class ($responseFactory) implements RequestHandlerInterface { + private ResponseFactoryInterface $responseFactory; + + public function __construct(ResponseFactoryInterface $responseFactory) + { + $this->responseFactory = $responseFactory; + } + + public function handle(ServerRequestInterface $request): ResponseInterface + { + $response = $this->responseFactory->createResponse(400); + $response->getBody()->write('Bad request.'); + return $response; + } +}; + +$middleware = new CsrfMiddleware($responseFactory, $csrfToken, $failureHandler); +``` + ## CSRF Tokens -In case Yii framework is used along with config plugin, the package is [configured](./config/web.php) automatically to use synchronizer token and masked decorator. You can change that depending on your needs. +In case Yii framework is used along with config plugin, the package is [configured](./config/web.php) +automatically to use synchronizer token and masked decorator. You can change that depending on your needs. ### Synchronizer CSRF token diff --git a/src/CsrfMiddleware.php b/src/CsrfMiddleware.php index 6b89fcd..21a00b2 100644 --- a/src/CsrfMiddleware.php +++ b/src/CsrfMiddleware.php @@ -30,24 +30,31 @@ final class CsrfMiddleware implements MiddlewareInterface private ResponseFactoryInterface $responseFactory; private CsrfTokenInterface $token; + private ?RequestHandlerInterface $failureHandler; public function __construct( ResponseFactoryInterface $responseFactory, - CsrfTokenInterface $token + CsrfTokenInterface $token, + RequestHandlerInterface $failureHandler = null ) { $this->responseFactory = $responseFactory; $this->token = $token; + $this->failureHandler = $failureHandler; } public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - if (!$this->validateCsrfToken($request)) { - $response = $this->responseFactory->createResponse(Status::UNPROCESSABLE_ENTITY); - $response->getBody()->write(Status::TEXTS[Status::UNPROCESSABLE_ENTITY]); - return $response; + if ($this->validateCsrfToken($request)) { + return $handler->handle($request); } - return $handler->handle($request); + if ($this->failureHandler !== null) { + return $this->failureHandler->handle($request); + } + + $response = $this->responseFactory->createResponse(Status::UNPROCESSABLE_ENTITY); + $response->getBody()->write(Status::TEXTS[Status::UNPROCESSABLE_ENTITY]); + return $response; } public function withParameterName(string $name): self diff --git a/tests/TokenCsrfMiddlewareTest.php b/tests/TokenCsrfMiddlewareTest.php index 70bac2d..d412e24 100644 --- a/tests/TokenCsrfMiddlewareTest.php +++ b/tests/TokenCsrfMiddlewareTest.php @@ -8,6 +8,7 @@ use Nyholm\Psr7\Response; use Nyholm\Psr7\ServerRequest; use PHPUnit\Framework\TestCase; +use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; use Yiisoft\Csrf\CsrfMiddleware; @@ -98,6 +99,28 @@ public function testInvalidTokenResultIn422(): void $this->assertEquals(Status::UNPROCESSABLE_ENTITY, $response->getStatusCode()); } + public function testInvalidTokenResultWithCustomFailureHandler(): void + { + $failureHandler = new class () implements RequestHandlerInterface { + public function handle(ServerRequestInterface $request): ResponseInterface + { + $response = new Response(Status::BAD_REQUEST); + $response->getBody()->write(Status::TEXTS[Status::BAD_REQUEST]); + return $response; + } + }; + + $middleware = $this->createCsrfMiddleware(null, $failureHandler); + + $response = $middleware->process( + $this->createPostServerRequestWithBodyToken(Random::string()), + $this->createRequestHandler(), + ); + + $this->assertEquals(Status::TEXTS[Status::BAD_REQUEST], $response->getBody()); + $this->assertEquals(Status::BAD_REQUEST, $response->getStatusCode()); + } + public function testEmptyTokenInRequestResultIn422(): void { $middleware = $this->createCsrfMiddleware(); @@ -155,12 +178,14 @@ private function getBodyRequestParamsByToken(string $token): array ]; } - protected function createCsrfMiddleware(?CsrfTokenInterface $csrfToken = null): CsrfMiddleware - { + protected function createCsrfMiddleware( + ?CsrfTokenInterface $csrfToken = null, + RequestHandlerInterface $failureHandler = null + ): CsrfMiddleware { $csrfToken = new MaskedCsrfToken($csrfToken ?? $this->createCsrfToken()); $this->token = $csrfToken->getValue(); - $middleware = new CsrfMiddleware(new Psr17Factory(), $csrfToken); + $middleware = new CsrfMiddleware(new Psr17Factory(), $csrfToken, $failureHandler); return $middleware->withParameterName(self::PARAM_NAME); }