Skip to content

Commit

Permalink
Fix #30: Add a custom failure handler feature to CsrfMiddleware
Browse files Browse the repository at this point in the history
Co-authored-by: Evgeniy Zyubin <[email protected]>
  • Loading branch information
solventt and devanych authored Nov 22, 2021
1 parent 76baf27 commit e7a091c
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 14 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
42 changes: 38 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ return [
->withMiddlewares(
[
ErrorCatcher::class,
SessionMiddleware::class, // <-- add this
CsrfMiddleware::class,
SessionMiddleware::class,
CsrfMiddleware::class, // <-- add this
Router::class,
]
);
Expand All @@ -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

Expand Down
19 changes: 13 additions & 6 deletions src/CsrfMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 28 additions & 3 deletions tests/TokenCsrfMiddlewareTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
}
Expand Down

0 comments on commit e7a091c

Please sign in to comment.