Search code examples
phpcakephpphpunitcakephp-3.7

CSRF token mismatch in cakephp3.7 tests


tests/TestCase/Controller/FeedbackControllerTest.php:45

public function testAdd()
{
    $this->enableCsrfToken();
    $this->enableSecurityToken();
    $this->session([
        'Auth' => [
            'User' => [
                'id' => 1,
                'role' => 'REPR',
            ]
        ]
    ]);
    $this->configRequest([
        'headers' => ['Accept' => 'application/json']
    ]);
    $_data = [
        'crash' => 1,
        'details' => 'Lorem ipsum dolor sit amet'
    ];
    $_data = json_encode($_data, JSON_PRETTY_PRINT);
    $this->post('/feedback/add', $_data); // <---- 45
    $expected = [
        'status' => 'success'
    ];
    $expected = json_encode($expected, JSON_PRETTY_PRINT);
    $this->assertEquals($expected, (string)$this->_response->getBody());
}

PHPUnit output:

1) App\Test\TestCase\Controller\FeedbackControllerTest::testAdd
Cake\Http\Exception\InvalidCsrfTokenException: Missing CSRF token cookie

/vagrant/vendor/cakephp/cakephp/src/Http/Middleware/CsrfProtectionMiddleware.php:196
/vagrant/vendor/cakephp/cakephp/src/Http/Middleware/CsrfProtectionMiddleware.php:120
/vagrant/vendor/cakephp/cakephp/src/Http/Middleware/CsrfProtectionMiddleware.php:106
/vagrant/vendor/cakephp/cakephp/src/Http/Runner.php:65
/vagrant/vendor/cakephp/cakephp/src/Http/Runner.php:51
/vagrant/vendor/cakephp/cakephp/src/Routing/Middleware/RoutingMiddleware.php:168
/vagrant/vendor/cakephp/cakephp/src/Http/Runner.php:65
/vagrant/vendor/cakephp/cakephp/src/Routing/Middleware/AssetMiddleware.php:88
/vagrant/vendor/cakephp/cakephp/src/Http/Runner.php:65
/vagrant/vendor/cakephp/cakephp/src/Error/Middleware/ErrorHandlerMiddleware.php:96
/vagrant/vendor/cakephp/cakephp/src/Http/Runner.php:65
/vagrant/vendor/cakephp/cakephp/src/Http/Runner.php:51
/vagrant/vendor/cakephp/cakephp/src/Http/Server.php:98
/vagrant/vendor/cakephp/cakephp/src/TestSuite/MiddlewareDispatcher.php:201
/vagrant/vendor/cakephp/cakephp/src/TestSuite/IntegrationTestTrait.php:516
/vagrant/vendor/cakephp/cakephp/src/TestSuite/IntegrationTestTrait.php:413
/vagrant/tests/TestCase/Controller/FeedbackControllerTest.php:45

I Have read and try solutions from answers:

How to create CSRF token for Cakephp 3 PHPunit testing?

if i add like @ndm says:

$token = 'my-csrf-token';

$this->cookie('csrfToken', $token);

$data = [
    'email' => '[email protected]',
    'password' => 'secret',
    '_csrfToken' => $token
];

Then:

Cake\Http\Exception\InvalidCsrfTokenException: CSRF token mismatch.

How to fix ?


Solution

  • When passing a string as POST data, the intergration test case won't automatically set tokens, neither the CSRF token, nor the security token, as it cannot inject anything into the string without knowing about the data format. Consequently it will also not set the cookie.

    So in cases where you pass string data, you have to set the cookie and the token manually, similarily as to described in the answer that you've linked. However when using anything other than application/x-www-form-urlencoded data (ie data that PHP will decode and put in the $_POST superglobal), in your example JSON data, you have to pass the token as a header, because the JSON input data will be decoded by the request handler component (there are plans to move this into the middleware layer IIRC), which runs after the CSRF middleware, which consequently won't see any post data.

    Example:

    $token = 'my-csrf-token';
    $this->cookie('csrfToken', $token);
    
    $this->configRequest([
        'headers' => [
            'X-CSRF-Token' => $token,
            // ...
        ]
    ]);
    

    Security component tokens on the other hand would have to go in the POST data, the security component won't look for headers, and it will have access to the decoded data after the request handler component has run (make sure that you load the request handler component before the security component!). You can refer to the \Cake\TestSuite\IntegrationTestTrait::_addTokens() source to figure how security tokens are built, you'd do it somewhat like this:

    $url = '/feedback/add';
    
    $_data = [
        'crash' => 1,
        'details' => 'Lorem ipsum dolor sit amet'
    ];
    
    $keys = array_map(
        function ($field) {
            return preg_replace('/(\.\d+)+$/', '', $field);
        },
        array_keys(Hash::flatten($_data))
    );
    
    $tokenData = $this->_buildFieldToken($url, array_unique($keys));
    
    $_data['_Token'] = $tokenData;
    $_data['_Token']['debug'] = 'SecurityComponent debug data would be added here';
    

    Note that the URL that is passed to _buildFieldToken() would also have to include possible query string data!