Repository: Ocramius/PSR7Csrf
Branch: master
Commit: 130e2aeb7350
Files: 38
Total size: 57.0 KB
Directory structure:
gitextract_rj5xlyxv/
├── .gitattributes
├── .gitignore
├── .scrutinizer.yml
├── .travis.yml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── composer.json
├── examples/
│ ├── .gitignore
│ ├── composer.json
│ └── index.php
├── humbug.json.dist
├── phpcs.xml.dist
├── phpunit.xml.dist
├── src/
│ └── PSR7Csrf/
│ ├── CSRFCheckerMiddleware.php
│ ├── Exception/
│ │ ├── ExceptionInterface.php
│ │ ├── InvalidExpirationTimeException.php
│ │ ├── InvalidRequestParameterNameException.php
│ │ └── SessionAttributeNotFoundException.php
│ ├── Factory.php
│ ├── HttpMethod/
│ │ ├── IsSafeHttpRequest.php
│ │ └── IsSafeHttpRequestInterface.php
│ ├── RequestParameter/
│ │ ├── ExtractCSRFParameter.php
│ │ └── ExtractCSRFParameterInterface.php
│ ├── Session/
│ │ ├── ExtractUniqueKeyFromSession.php
│ │ └── ExtractUniqueKeyFromSessionInterface.php
│ ├── TokenGenerator.php
│ └── TokenGeneratorInterface.php
└── test/
└── PSR7CsrfTest/
├── CSRFCheckerMiddlewareTest.php
├── Exception/
│ ├── InvalidExpirationTimeExceptionTest.php
│ ├── InvalidRequestParameterNameExceptionTest.php
│ └── SessionAttributeNotFoundExceptionTest.php
├── FactoryTest.php
├── HttpMethod/
│ └── IsSafeHttpRequestTest.php
├── RequestParameter/
│ └── ExtractCSRFParameterTest.php
├── Session/
│ └── ExtractUniqueKeyFromSessionTest.php
└── TokenGeneratorTest.php
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitattributes
================================================
/test export-ignore
/examples export-ignore
.gitattributes export-ignore
.gitignore export-ignore
.scrutinizer.yml export-ignore
.travis.yml export-ignore
phpunit.xml.dist
================================================
FILE: .gitignore
================================================
vendor
composer.lock
composer.phar
clover.xml
humbug-log.json
humbug-log.txt
humbug
================================================
FILE: .scrutinizer.yml
================================================
before_commands:
- "composer update --prefer-source"
tools:
external_code_coverage:
timeout: 1800
php_code_coverage:
enabled: true
php_code_sniffer:
enabled: false
php_cpd:
enabled: true
excluded_dirs: ["test", "vendor"]
php_cs_fixer:
enabled: true
config:
level: all
filter:
paths: ["src/*", "test/*"]
php_loc:
enabled: true
excluded_dirs: ["test", "vendor"]
php_mess_detector:
enabled: true
filter:
paths: ["src/*"]
php_pdepend:
enabled: true
excluded_dirs: ["test", "vendor"]
php_analyzer:
enabled: true
filter:
paths: ["src/*", "test/*"]
php_hhvm:
enabled: true
filter:
paths: ["src/*", "test/*"]
sensiolabs_security_checker: true
================================================
FILE: .travis.yml
================================================
language: php
php:
- 7.1
- 7.2
- nightly
before_script:
- composer update
- if [ "$DEPENDENCIES" = 'low' ] ; then composer update --prefer-source --prefer-lowest --prefer-stable; fi
- git clone https://github.com/padraic/humbug.git && cd humbug && composer install && cd ..
script:
- if [[ $TRAVIS_PHP_VERSION = '7.1' ]]; then PHPUNIT_FLAGS="--coverage-clover ./clover.xml"; else PHPUNIT_FLAGS=""; fi
- ./vendor/bin/phpunit $PHPUNIT_FLAGS
- ./vendor/bin/phpcs
- if [[ $TRAVIS_PHP_VERSION = '7.1' ]]; then ./vendor/bin/humbug; fi
- cd examples
- composer install
- php index.php > /dev/null
env:
matrix:
- DEPENDENCIES="high"
- DEPENDENCIES="low"
matrix:
allow_failures:
- php: hhvm
after_script:
- wget https://scrutinizer-ci.com/ocular.phar
- if [ -f clover.xml ]; then php ocular.phar code-coverage:upload --format=php-clover ./clover.xml; fi
================================================
FILE: CHANGELOG.md
================================================
This is a list of changes/improvements that were introduced in PSR7Csrf
## 2.0.0
- This release aligns the `PSR7Csrf\CSRFCheckerMiddleware` implementation to
the [PSR-15 `php-fig/http-server-middleware`](https://github.com/php-fig/http-server-middleware/tree/1.0.0)
specification.
This means that the signature of `PSR7Csrf\CSRFCheckerMiddleware`
changed, and therefore you need to look for usages of this class and verify
if the new signature is compatible with your API
Specifically, `PSR7Csrf\CSRFCheckerMiddleware#__invoke()` was removed.
- The minimum supported PHP version has been raised to `7.1.0`
- the `PSR7Csrf\Factory::createDefaultCSRFCheckerMiddleware()` method now has
a mandatory argument, which is the response to be produced in case of failed
CSRF validation. This argument is mandatory, since PSR7Csrf won't couple you
to a specific PSR-7 implementation.
## 1.0.2
### Fixed
- Allow installation of [PSR7Session](https://github.com/Ocramius/PSR7Session)
[2.0.0](https://github.com/Ocramius/PSR7Session/releases/tag/2.0.0) [#2](https://github.com/Ocramius/PSR7Csrf/pull/1)
## 1.0.1
### Fixed
- Minor wording issues in [`README.md`](README.md] [#1](https://github.com/Ocramius/PSR7Csrf/pull/1)
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing
* Coding standard for the project is [PSR-2](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)
* The project will follow [object calisthenics](http://www.slideshare.net/guilhermeblanco/object-calisthenics-applied-to-php)
* Any contribution must provide tests for additional/corrected scenarios
* Any un-confirmed issue needs a failing test case before being accepted
* Pull requests must be sent from a new hotfix/feature branch, not from `master`.
## Installation
To install the project and run the tests, you need to clone it first:
```sh
$ git clone git://github.com/Ocramius/PSR7Csrf.git
```
You will then need to run a composer installation:
```sh
$ cd PSR7Csrf
$ curl -s https://getcomposer.org/installer | php
$ php composer.phar update
```
## Testing
The PHPUnit version to be used is the one installed as a dev- dependency via composer:
```sh
$ ./vendor/bin/phpunit
```
Accepted coverage for new contributions is 80%. Any contribution not satisfying this requirement
won't be merged.
================================================
FILE: LICENSE
================================================
Copyright (c) 2015 Marco Pivetta
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# PSR-7 Storage-less HTTP CSRF protection
[](https://travis-ci.org/Ocramius/PSR7Csrf)
[](https://scrutinizer-ci.com/g/Ocramius/PSR7Csrf/?branch=master)
[](https://scrutinizer-ci.com/g/Ocramius/PSR7Csrf/?branch=master)
[](https://packagist.org/packages/ocramius/psr7-csrf)
[](https://packagist.org/packages/ocramius/psr7-csrf)
**PSR7Csrf** is a [PSR-7](http://www.php-fig.org/psr/psr-7/)
[middleware](https://mwop.net/blog/2015-01-08-on-http-middleware-and-psr-7.html) that enables
[CSRF](https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)) protection for PSR-7 based applications.
# DEPRECATED in favor of `psr7-sessions/storageless` 5.0.0+
Please note that this package is **DEPRECATED**.
Since [`psr7-sessions/storageless` 5.0.0](https://github.com/psr7-sessions/storageless/releases/tag/5.0.0),
the generated cookies are CSRF-resistant by default for unsafe HTTP methods (`POST`/`PUT`/`DELETE`/`PATCH`/etc.),
so the usage of this package is no longer needed.
You can still install `ocramius/psr7-csrf`, but since there is no practical need for it,
it is not necessary to do so.
### What is this about?
Instead of storing tokens in the session, PSR7Csrf simply uses JWT tokens,
which can be verified, signed and have a specific lifetime on their own.
This storage-less approach prevents having to load tokens from a session
or from a database, and simplifies the entire UI workflow: tokens are
valid as long as their signature and expiration date holds.
### Installation
```sh
composer require ocramius/psr7-csrf
```
### Usage
The simplest usage is based on defaults. It assumes that you have
a configured PSR-7 compatible application that supports piping
middlewares, and it also requires you to run [PSR7Session](https://github.com/Ocramius/PSR7Session).
In a [`zendframework/zend-expressive`](https://github.com/zendframework/zend-expressive)
application, the setup would look like the following:
```php
$app = \Zend\Expressive\AppFactory::create();
$app->pipe(\PSR7Session\Http\SessionMiddleware::fromSymmetricKeyDefaults(
'mBC5v1sOKVvbdEitdSBenu59nfNfhwkedkJVNabosTw=', // replace this with a key of your own (see PSR7Session docs)
1200 // 20 minutes session duration
));
$app->pipe(\PSR7Csrf\Factory::createDefaultCSRFCheckerMiddleware());
```
This setup will require that any requests that are not `GET`, `HEAD` or
`OPTIONS` contain a `csrf_token` in the request body parameters (JSON
or URL-encoded).
You can generate the CSRF token for any form like following:
```php
$tokenGenerator = \PSR7Csrf\Factory::createDefaultTokenGenerator();
$app->get('/get', function ($request, $response) use ($tokenGenerator) {
$response
->getBody()
->write(
'
'
);
return $response;
});
$app->post('/post', function ($request, $response) {
$response
->getBody()
->write('It works!');
return $response;
});
```
### Examples
```sh
composer install # install at the root of this package first!
cd examples
composer install
php -S localhost:9999 index.php
```
Then try accessing `http://localhost:9999`: you should see a simple
submission form.
If you try modifying the submitted CSRF token (which is in a hidden
form field), then the `POST` request will fail.
### Known limitations
Please refer to the [known limitations of PSR7Session](https://github.com/Ocramius/PSR7Session/blob/master/docs/limitations.md).
Also, this component does *NOT* prevent double-form-submissions: it
merely prevents CSRF attacks from third parties. As long as the CSRF
token is valid, it can be reused over multiple requests.
### Contributing
Please refer to the [contributing notes](CONTRIBUTING.md).
### License
This project is made public under the [MIT LICENSE](LICENSE).
================================================
FILE: composer.json
================================================
{
"name": "ocramius/psr7-csrf",
"license": "MIT",
"authors": [
{
"name": "Marco Pivetta",
"email": "ocramius@gmail.com",
"homepage": "http://ocramius.github.io/",
"role": "Developer"
}
],
"require": {
"php": "^7.1.0",
"psr/http-message": "^1.0.1",
"lcobucci/jwt": "^3.2.2",
"psr/http-server-handler": "^1.0.0",
"psr/http-server-middleware": "^1.0.0",
"psr7-sessions/storageless": "^4.0.0"
},
"require-dev": {
"phpunit/phpunit": "^6.5.5",
"humbug/humbug": "^1.0.0-rc.0",
"squizlabs/php_codesniffer": "^2.6.0"
},
"autoload": {
"psr-4": {
"PSR7Csrf\\": "src/PSR7Csrf"
}
},
"autoload-dev": {
"psr-4": {
"PSR7CsrfTest\\": "test/PSR7CsrfTest"
}
},
"extra": {
"branch-alias": {
"dev-master": "3.0.x-dev"
}
}
}
================================================
FILE: examples/.gitignore
================================================
vendor
composer.lock
================================================
FILE: examples/composer.json
================================================
{
"require": {
"zendframework/zend-diactoros": "^1.7.0"
},
"autoload": {
"files": [
"../vendor/autoload.php"
]
}
}
================================================
FILE: examples/index.php
================================================
withSecure(false)// false on purpose, unless you have https locally - DO NOT DO THIS IN PRODUCTION!
->withHttpOnly(true),
new Parser(),
1200,
new SystemClock()
);
/**
* This is the actual component from this package. Setup assumes that
* a `SessionMiddleware` was previously piped through. If you don't do that,
* then all requests will fail CSRF validation!
*/
$csrfMiddleware = Factory::createDefaultCSRFCheckerMiddleware(
(new JsonResponse(['error' => 'CSRF validation failed']))->withStatus(401)
);
/**
* The token generator is needed to generate CSRF tokens to be added to your forms
*/
$tokenGenerator = Factory::createDefaultTokenGenerator();
/**
* This is an example of how you'd generate a form with a CSRF token from this package
*/
$action = new class ($tokenGenerator) implements RequestHandlerInterface
{
/**
* @var TokenGeneratorInterface
*/
private $tokenGenerator;
public function __construct(TokenGeneratorInterface $tokenGenerator)
{
$this->tokenGenerator = $tokenGenerator;
}
public function handle(ServerRequestInterface $request) : ResponseInterface
{
if ('GET' === \strtoupper($request->getMethod())) {
return new HtmlResponse(
''
. ''
);
}
return new TextResponse('It works!');
}
};
/**
* Don't panic! This just emulates what a typical middleware-based HTTP framework does internally
*/
$pipe = new class ($action, $sessionMiddleware, $csrfMiddleware) implements RequestHandlerInterface
{
/**
* @var RequestHandlerInterface
*/
private $action;
/**
* @var MiddlewareInterface[]
*/
private $pipedMiddleware;
public function __construct(RequestHandlerInterface $action, MiddlewareInterface ...$pipedMiddleware)
{
$this->action = $action;
$this->pipedMiddleware = $pipedMiddleware;
}
public function handle(ServerRequestInterface $request) : ResponseInterface
{
if (! $this->pipedMiddleware) {
return $this->action->handle($request);
}
return $this->pipedMiddleware[0]->process(
$request,
new self($this->action, ...\array_values(\array_slice($this->pipedMiddleware, 1)))
);
}
};
// produce the response
(new SapiEmitter())
->emit($pipe->handle(ServerRequestFactory::fromGlobals()));
================================================
FILE: humbug.json.dist
================================================
{
"timeout": 30,
"source": {
"directories": [
"src"
]
},
"logs": {
"text": "humbug-log.txt",
"json": "humbug-log.json"
}
}
================================================
FILE: phpcs.xml.dist
================================================
code-reviews.io code-style
./src
./test
./test/PSR7CsrfTest/RequestParameter/ExtractCSRFParameterTest.php
================================================
FILE: phpunit.xml.dist
================================================
./test/PSR7CsrfTest
./src
================================================
FILE: src/PSR7Csrf/CSRFCheckerMiddleware.php
================================================
isSafeHttpRequest = $isSafeHttpRequest;
$this->extractUniqueKeyFromSession = $extractUniqueKeyFromSession;
$this->extractCSRFParameter = $extractCSRFParameter;
$this->tokenParser = $tokenParser;
$this->signer = $signer;
$this->sessionAttribute = $sessionAttribute;
$this->faultyResponse = $faultyResponse;
}
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
) : ResponseInterface {
if ($this->isSafeHttpRequest->__invoke($request)) {
return $handler->handle($request);
}
try {
$token = $this->tokenParser->parse($this->extractCSRFParameter->__invoke($request));
if ($token->validate(new ValidationData())
&& $token->verify(
$this->signer,
$this->extractUniqueKeyFromSession->__invoke($this->getSession($request))
)
) {
return $handler->handle($request);
}
} catch (BadMethodCallException $invalidToken) {
return $this->faultyResponse;
} catch (InvalidArgumentException $invalidToken) {
return $this->faultyResponse;
}
return $this->faultyResponse;
}
private function getSession(ServerRequestInterface $request) : SessionInterface
{
$session = $request->getAttribute($this->sessionAttribute);
if (! $session instanceof SessionInterface) {
throw SessionAttributeNotFoundException::fromAttributeNameAndRequest($this->sessionAttribute, $request);
}
return $session;
}
}
================================================
FILE: src/PSR7Csrf/Exception/ExceptionInterface.php
================================================
0 integer', $expirationTime));
}
}
================================================
FILE: src/PSR7Csrf/Exception/InvalidRequestParameterNameException.php
================================================
getAttributes()))
));
}
}
================================================
FILE: src/PSR7Csrf/Factory.php
================================================
safeMethods = array_map('strtoupper', $safeMethods);
}
public static function fromDefaultSafeMethods() : self
{
return new self('GET', 'HEAD', 'OPTIONS');
}
public function __invoke(RequestInterface $request) : bool
{
return in_array(strtoupper($request->getMethod()), $this->safeMethods, self::STRICT_CHECKING);
}
}
================================================
FILE: src/PSR7Csrf/HttpMethod/IsSafeHttpRequestInterface.php
================================================
csrfDataKey = $csrfDataKey;
}
public function __invoke(ServerRequestInterface $request) : string
{
/* @var $requestBody array */
$requestBody = $request->getParsedBody();
if (is_object($requestBody) && array_key_exists($this->csrfDataKey, (array) $requestBody)) {
$arrayBody = (array) $requestBody;
return $this->ensureThatTheValueIsAString($arrayBody[$this->csrfDataKey]);
}
if (is_array($requestBody) && array_key_exists($this->csrfDataKey, $requestBody)) {
return $this->ensureThatTheValueIsAString($requestBody[$this->csrfDataKey]);
}
return '';
}
private function ensureThatTheValueIsAString($value) : string
{
if (! is_string($value)) {
return '';
}
return $value;
}
}
================================================
FILE: src/PSR7Csrf/RequestParameter/ExtractCSRFParameterInterface.php
================================================
uniqueIdKey = $uniqueIdKey;
}
public function __invoke(SessionInterface $session) : string
{
$uniqueKey = $session->get($this->uniqueIdKey, '');
if ('' === $uniqueKey || ! is_string($uniqueKey)) {
$generatedKey = bin2hex(random_bytes(self::ENTROPY));
$session->set($this->uniqueIdKey, $generatedKey);
return $generatedKey;
}
return $uniqueKey;
}
}
================================================
FILE: src/PSR7Csrf/Session/ExtractUniqueKeyFromSessionInterface.php
================================================
signer = $signer;
$this->extractUniqueKeyFromSession = $extractUniqueKeyFromSession;
$this->expirationTime = $expirationTime;
$this->sessionAttribute = $sessionAttribute;
}
public function __invoke(ServerRequestInterface $request) : Token
{
$session = $request->getAttribute($this->sessionAttribute);
if (! $session instanceof SessionInterface) {
throw SessionAttributeNotFoundException::fromAttributeNameAndRequest($this->sessionAttribute, $request);
}
$timestamp = (new \DateTime())->getTimestamp();
return (new Builder())
->setIssuedAt($timestamp)
->setExpiration($timestamp + $this->expirationTime)
->sign($this->signer, $this->extractUniqueKeyFromSession->__invoke($session))
->getToken();
}
}
================================================
FILE: src/PSR7Csrf/TokenGeneratorInterface.php
================================================
signer = new Signer\Hmac\Sha256();
$this->tokenParser = new Parser();
$this->isSafeHttpRequest = $this->createMock(IsSafeHttpRequestInterface::class);
$this->extractUniqueKeyFromSession = $this->createMock(ExtractUniqueKeyFromSessionInterface::class);
$this->extractCSRFParameter = $this->createMock(ExtractCSRFParameterInterface::class);
$this->request = $this->createMock(ServerRequestInterface::class);
$this->response = $this->createMock(ResponseInterface::class);
$this->session = $this->createMock(SessionInterface::class);
$this->sessionAttribute = uniqid('session', true);
$this->nextMiddleware = $this->createMock(RequestHandlerInterface::class);
$this->faultyResponse = $this->createMock(ResponseInterface::class);
$this->middleware = new CSRFCheckerMiddleware(
$this->isSafeHttpRequest,
$this->extractUniqueKeyFromSession,
$this->extractCSRFParameter,
$this->tokenParser,
$this->signer,
$this->sessionAttribute,
$this->faultyResponse
);
}
public function testWillIgnoreSafeRequestsWithNoNextMiddleware()
{
$this->isSafeHttpRequest->expects(self::any())->method('__invoke')->with($this->request)->willReturn(true);
$this
->nextMiddleware
->expects(self::once())
->method('handle')
->with($this->request)
->willReturn($this->response);
self::assertSame($this->response, $this->middleware->process($this->request, $this->nextMiddleware));
}
public function testWillSucceedIfANonSafeRequestIsProvidedWithAValidTokenWithNextMiddleware()
{
$secret = uniqid('secret', true);
$validToken = (new Builder())
->sign($this->signer, $secret)
->getToken();
$this
->nextMiddleware
->expects(self::once())
->method('handle')
->with($this->request)
->willReturn($this->response);
$this->isSafeHttpRequest->expects(self::any())->method('__invoke')->with($this->request)->willReturn(false);
$this
->extractUniqueKeyFromSession
->expects(self::any())
->method('__invoke')
->with($this->session)
->willReturn($secret);
$this
->extractCSRFParameter
->expects(self::any())
->method('__invoke')
->with($this->request)
->willReturn((string) $validToken);
$this
->request
->expects(self::any())
->method('getAttribute')
->with($this->sessionAttribute)
->willReturn($this->session);
self::assertSame(
$this->response,
$this->middleware->process($this->request, $this->nextMiddleware)
);
}
public function testNonMatchingSignedTokensAreRejected()
{
$secret = uniqid('secret', true);
$validToken = (new Builder())
->sign($this->signer, uniqid('wrongSecret', true))
->getToken();
$this->isSafeHttpRequest->expects(self::any())->method('__invoke')->with($this->request)->willReturn(false);
$this
->extractUniqueKeyFromSession
->expects(self::any())
->method('__invoke')
->with($this->session)
->willReturn($secret);
$this
->extractCSRFParameter
->expects(self::any())
->method('__invoke')
->with($this->request)
->willReturn((string) $validToken);
$this
->request
->expects(self::any())
->method('getAttribute')
->with($this->sessionAttribute)
->willReturn($this->session);
$this->assertFaultyResponse();
}
public function testUnsignedTokensAreRejected()
{
$secret = uniqid('secret', true);
$validToken = (new Builder())->getToken();
$this->isSafeHttpRequest->expects(self::any())->method('__invoke')->with($this->request)->willReturn(false);
$this
->extractUniqueKeyFromSession
->expects(self::any())
->method('__invoke')
->with($this->session)
->willReturn($secret);
$this
->extractCSRFParameter
->expects(self::any())
->method('__invoke')
->with($this->request)
->willReturn((string) $validToken);
$this
->request
->expects(self::any())
->method('getAttribute')
->with($this->sessionAttribute)
->willReturn($this->session);
$this->assertFaultyResponse();
}
public function testExpiredSignedTokensAreRejected()
{
$secret = uniqid('secret', true);
$validToken = (new Builder())
->setExpiration(time() - 3600)
->sign($this->signer, $secret)
->getToken();
$this->isSafeHttpRequest->expects(self::any())->method('__invoke')->with($this->request)->willReturn(false);
$this
->extractUniqueKeyFromSession
->expects(self::any())
->method('__invoke')
->with($this->session)
->willReturn($secret);
$this
->extractCSRFParameter
->expects(self::any())
->method('__invoke')
->with($this->request)
->willReturn((string) $validToken);
$this
->request
->expects(self::any())
->method('getAttribute')
->with($this->sessionAttribute)
->willReturn($this->session);
$this->assertFaultyResponse();
}
public function testMalformedTokensShouldBeRejected()
{
$this->isSafeHttpRequest->expects(self::any())->method('__invoke')->with($this->request)->willReturn(false);
$this
->extractUniqueKeyFromSession
->expects(self::any())
->method('__invoke')
->with($this->session)
->willReturn(uniqid('secret', true));
$this
->extractCSRFParameter
->expects(self::any())
->method('__invoke')
->with($this->request)
->willReturn('yadda yadda invalid bs');
$this
->request
->expects(self::any())
->method('getAttribute')
->with($this->sessionAttribute)
->willReturn($this->session);
$this->assertFaultyResponse();
}
public function testWillFailIfARequestDoesNotIncludeASession()
{
$this->isSafeHttpRequest->expects(self::any())->method('__invoke')->with($this->request)->willReturn(false);
$this
->extractCSRFParameter
->expects(self::any())
->method('__invoke')
->with($this->request)
->willReturn((new Builder())->getToken());
$this
->request
->expects(self::any())
->method('getAttribute')
->with($this->sessionAttribute)
->willReturn(new stdClass());
$this
->request
->expects(self::any())
->method('getAttributes')
->willReturn([]);
$this->expectException(SessionAttributeNotFoundException::class);
$this->middleware->process($this->request, $this->nextMiddleware);
}
private function assertFaultyResponse() : void
{
$this->nextMiddleware->expects(self::never())->method('handle');
self::assertSame($this->faultyResponse, $this->middleware->process($this->request, $this->nextMiddleware));
}
}
================================================
FILE: test/PSR7CsrfTest/Exception/InvalidExpirationTimeExceptionTest.php
================================================
0 integer', $exception->getMessage());
}
}
================================================
FILE: test/PSR7CsrfTest/Exception/InvalidRequestParameterNameExceptionTest.php
================================================
getMessage());
}
}
================================================
FILE: test/PSR7CsrfTest/Exception/SessionAttributeNotFoundExceptionTest.php
================================================
createMock(ServerRequestInterface::class);
$request->expects(self::any())->method('getAttributes')->willReturn(['foo' => 'bar', 'baz' => 'tab']);
$exception = SessionAttributeNotFoundException::fromAttributeNameAndRequest('foo', $request);
self::assertInstanceOf(SessionAttributeNotFoundException::class, $exception);
self::assertInstanceOf(UnexpectedValueException::class, $exception);
self::assertInstanceOf(ExceptionInterface::class, $exception);
self::assertSame(
'Provided request contains no matching session attribute "foo", attributes ["foo","baz"] exist',
$exception->getMessage()
);
}
}
================================================
FILE: test/PSR7CsrfTest/FactoryTest.php
================================================
createMock(ResponseInterface::class);
$middleware = Factory::createDefaultCSRFCheckerMiddleware($faultyResponse);
self::assertInstanceOf(CSRFCheckerMiddleware::class, $middleware);
$request = $this->createMock(ServerRequestInterface::class);
$request
->expects(self::any())
->method('getMethod')
->willReturn('POST');
self::assertSame(
$faultyResponse,
$middleware->process(
$request,
$this->createMock(RequestHandlerInterface::class)
),
'Faulty http response passed to the factory is returned as part of a failed CSRF validation'
);
}
public function testCreateDefaultTokenGenerator()
{
self::assertInstanceOf(TokenGeneratorInterface::class, Factory::createDefaultTokenGenerator());
}
}
================================================
FILE: test/PSR7CsrfTest/HttpMethod/IsSafeHttpRequestTest.php
================================================
createMock(RequestInterface::class);
$request->expects(self::any())->method('getMethod')->willReturn($httpMethod);
self::assertSame($expectedResult, (new IsSafeHttpRequest(...$safeMethods))->__invoke($request));
}
public function httpMethodsProvider() : array
{
return [
'empty' => [
[],
'GET',
false,
],
'GET only' => [
['GET'],
'GET',
true,
],
'get only' => [
['get'],
'GET',
true,
],
'GET only, matching lowercase get' => [
['GET'],
'get',
true,
],
'GET only, non-matching method' => [
['GET'],
'PUT',
false,
],
'GET, PUT only, matching method' => [
['GET', 'PUT'],
'PUT',
true,
],
];
}
/**
* @dataProvider safeDefaultsMatchingProvider
*
* @param string $httpMethod
* @param bool $expectedResult
*/
public function testSafeMethodsWithDefaults(string $httpMethod, bool $expectedResult)
{
/* @var $request RequestInterface|\PHPUnit_Framework_MockObject_MockObject */
$request = $this->createMock(RequestInterface::class);
$request->expects(self::any())->method('getMethod')->willReturn($httpMethod);
self::assertSame($expectedResult, IsSafeHttpRequest::fromDefaultSafeMethods()->__invoke($request));
}
public function safeDefaultsMatchingProvider() : array
{
return [
'empty' => [
'',
false,
],
'GET' => [
'GET',
true,
],
'get' => [
'get',
true,
],
'HEAD' => [
'HEAD',
true,
],
'head' => [
'head',
true,
],
'OPTIONS' => [
'OPTIONS',
true,
],
'options' => [
'options',
true,
],
'DELETE' => [
'DELETE',
false,
],
'delete' => [
'delete',
false,
],
'POST' => [
'POST',
false,
],
'post' => [
'post',
false,
],
'PUT' => [
'PUT',
false,
],
'put' => [
'put',
false,
],
'UNKNOWN' => [
'UNKNOWN',
false,
],
'unknown' => [
'unknown',
false,
],
];
}
}
================================================
FILE: test/PSR7CsrfTest/RequestParameter/ExtractCSRFParameterTest.php
================================================
expectException(InvalidRequestParameterNameException::class);
new ExtractCSRFParameter('');
}
/**
* @dataProvider requestBodyProvider
*
* @param string $requestParameter
* @param null|object|array $body
* @param string $expectedExtractedValue
*
* @return void
*/
public function testExtraction(string $requestParameter, $body, string $expectedExtractedValue)
{
/* @var $request ServerRequestInterface|\PHPUnit_Framework_MockObject_MockObject */
$request = $this->createMock(ServerRequestInterface::class);
$request->expects(self::any())->method('getParsedBody')->willReturn($body);
self::assertSame($expectedExtractedValue, (new ExtractCSRFParameter($requestParameter))->__invoke($request));
}
public function requestBodyProvider()
{
/** @noinspection PhpUnusedPrivateFieldInspection */
return [
'null body' => [
'request parameter name',
null,
'',
],
'empty array' => [
'request parameter name',
[],
'',
],
'empty object' => [
'request parameter name',
(object) [],
'',
],
'array with matching parameter' => [
'request parameter name',
['request parameter name' => 'foo'],
'foo',
],
'array with matching non-string parameter' => [
'request parameter name',
['request parameter name' => 123],
'',
],
'object with matching parameter' => [
'request parameter name',
(object) ['request parameter name' => 'foo'],
'foo',
],
'object with matching non-string parameter' => [
'request parameter name',
(object) ['request parameter name' => 123],
'',
],
'class with private matching property' => [
'field',
new class {
private $field = 'bar';
},
'',
],
'class with protected matching property' => [
'field',
new class {
protected $field = 'bar';
},
'',
],
'class with public matching property' => [
'field',
new class {
public $field = 'bar';
},
'bar',
],
'class with public matching non-string property' => [
'field',
new class {
public $field = 123;
},
'',
],
];
}
}
================================================
FILE: test/PSR7CsrfTest/Session/ExtractUniqueKeyFromSessionTest.php
================================================
createMock(SessionInterface::class);
$superSecret = uniqid('', true);
$session->expects(self::any())->method('get')->with($key, '')->willReturn($superSecret);
$session->expects(self::never())->method('set');
self::assertSame($superSecret, (new ExtractUniqueKeyFromSession($key))->__invoke($session));
}
/**
* @dataProvider keysProvider
*
* @param string $key
*/
public function testExtractionWithEmptyExistingKey(string $key)
{
$extractKey = new ExtractUniqueKeyFromSession($key);
/* @var $session SessionInterface|\PHPUnit_Framework_MockObject_MockObject */
$session = $this->createMock(SessionInterface::class);
$session->expects(self::any())->method('get')->with($key, '')->willReturn('');
$session->expects(self::exactly(2))->method('set')->with(
$key,
self::callback(function (string $secret) {
self::assertNotEmpty($secret);
return true;
})
);
$secretUniqueKey = $extractKey->__invoke($session);
self::assertInternalType('string', $secretUniqueKey);
self::assertNotEmpty($secretUniqueKey);
$anotherSecretKey = $extractKey->__invoke($session);
self::assertInternalType('string', $anotherSecretKey);
self::assertNotEmpty($anotherSecretKey);
self::assertNotEquals($secretUniqueKey, $anotherSecretKey);
}
/**
* @dataProvider keysProvider
*
* @param string $key
*/
public function testExtractionWithNonStringExistingKey(string $key)
{
$extractKey = new ExtractUniqueKeyFromSession($key);
/* @var $session SessionInterface|\PHPUnit_Framework_MockObject_MockObject */
$session = $this->createMock(SessionInterface::class);
$session->expects(self::any())->method('get')->with($key, '')->willReturn(123);
$session->expects(self::exactly(2))->method('set')->with(
$key,
self::callback(function (string $secret) {
self::assertNotEmpty($secret);
return true;
})
);
$secretUniqueKey = $extractKey->__invoke($session);
self::assertInternalType('string', $secretUniqueKey);
self::assertNotEmpty($secretUniqueKey);
$anotherSecretKey = $extractKey->__invoke($session);
self::assertInternalType('string', $anotherSecretKey);
self::assertNotEmpty($anotherSecretKey);
self::assertNotEquals($secretUniqueKey, $anotherSecretKey);
}
public function keysProvider() : array
{
return [
[''],
['key'],
['123'],
['123 456'],
];
}
}
================================================
FILE: test/PSR7CsrfTest/TokenGeneratorTest.php
================================================
createMock(Signer::class);
/* @var $extractUniqueKeyFromSession ExtractUniqueKeyFromSessionInterface */
$extractUniqueKeyFromSession = $this->createMock(ExtractUniqueKeyFromSessionInterface::class);
if (! $valid) {
$this->expectException(InvalidExpirationTimeException::class);
}
self::assertInstanceOf(
TokenGenerator::class,
new TokenGenerator($signer, $extractUniqueKeyFromSession, $invalidExpirationTime, 'session')
);
}
public function invalidExpirationTimeProvider() : array
{
return [
[100, true],
[1, true],
[0, false],
[-1, false],
[-200, false],
];
}
/**
* @dataProvider validExpirationTimeProvider
*
* @param int $validExpirationTime
*/
public function testWillGenerateAValidJWTToken(int $validExpirationTime)
{
$signer = new Sha256();
/* @var $extractUniqueKeyFromSession ExtractUniqueKeyFromSessionInterface|\PHPUnit_Framework_MockObject_MockObject */
$extractUniqueKeyFromSession = $this->createMock(ExtractUniqueKeyFromSessionInterface::class);
/* @var $session SessionInterface */
$session = $this->createMock(SessionInterface::class);
/* @var $request ServerRequestInterface|\PHPUnit_Framework_MockObject_MockObject */
$request = $this->createMock(ServerRequestInterface::class);
$sessionAttribute = uniqid('session', true);
$generator = new TokenGenerator($signer, $extractUniqueKeyFromSession, $validExpirationTime, $sessionAttribute);
$secretKey = uniqid('secretKey', true);
$request->expects(self::any())->method('getAttribute')->with($sessionAttribute)->willReturn($session);
$extractUniqueKeyFromSession->expects(self::any())->method('__invoke')->with($session)->willReturn($secretKey);
$token = $generator->__invoke($request);
self::assertTrue($token->verify($signer, $secretKey));
self::assertLessThanOrEqual(time(), $token->getClaim('iat'));
self::assertGreaterThan(time(), $token->getClaim('exp'));
}
public function validExpirationTimeProvider() : array
{
return [
[10],
[100],
];
}
public function testWillFailIfTheSessionAttributeIsNotASession()
{
/* @var $extractUniqueKeyFromSession ExtractUniqueKeyFromSessionInterface|\PHPUnit_Framework_MockObject_MockObject */
$extractUniqueKeyFromSession = $this->createMock(ExtractUniqueKeyFromSessionInterface::class);
/* @var $request ServerRequestInterface|\PHPUnit_Framework_MockObject_MockObject */
$request = $this->createMock(ServerRequestInterface::class);
$sessionAttribute = uniqid('session', true);
$generator = new TokenGenerator(new Sha256(), $extractUniqueKeyFromSession, 10, $sessionAttribute);
$request->expects(self::any())->method('getAttribute')->with($sessionAttribute)->willReturn(new stdClass());
$request->expects(self::any())->method('getAttributes')->willReturn([]);
$this->expectException(SessionAttributeNotFoundException::class);
$generator->__invoke($request);
}
}