8

I'm trying to test an ajax request in Symfony2. I'm writing a unit test which is throwing the following error in my app/logs/test.log:

request.CRITICAL: Uncaught PHP Exception Twig_Error_Runtime: 
"Impossible to access an attribute ("0") on a string variable 
("The CSRF token is invalid. Please try to resubmit the form.")
in .../vendor/twig/twig/lib/Twig/Template.php:388

My code is fairly straight-forward.

public function testAjaxJsonResponse()
{
    $form['post']['title'] = 'test title';
    $form['post']['content'] = 'test content';
    $form['post']['_token'] = $client->getContainer()->get('form.csrf_provider')->generateCsrfToken();

    $client->request('POST', '/path/to/ajax/', $form, array(), array(
        'HTTP_X-Requested-With' => 'XMLHttpRequest',
    ));

    $response = $client->getResponse();
    $this->assertSame(200, $client->getResponse()->getStatusCode());
    $this->assertSame('application/json', $response->headers->get('Content-Type'));
}

The issue seems to be the CSRF token, I could disable it for the tests, but I don't really want to do that, I had it working by making 2 requests (the first one loads the page with the form, we grab the _token and make a second request using with XMLHttpRequest) - This obviously seems rather silly and inefficient!

2 Answers 2

9

Solution

We can generate our own CSRF token for our ajax request with:

$client->getContainer()->get('form.csrf_provider')->generateCsrfToken($intention);

Here the variable $intention refers to an array key set in your Form Type Options.

Add the intention

In your Form Type you will need to add the intention key. e.g:

# AcmeBundle\Form\Type\PostType.php

/**
 *  Additional fields (if you want to edit them), the values shown are the default
 * 
 * 'csrf_protection' => true,
 * 'csrf_field_name' => '_token', // This must match in your test
 *
 * @param OptionsResolverInterface $resolver
 */
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'data_class' => 'Acme\AcmeBundle\Entity\Post',
        // a unique key to help generate the secret token
        'intention' => 'post_type',
    ));
}

Read the documentation

Generate the CSRF Token in your Functional test

Now we have that intention, we can use it in our unit test to generate a valid CSRF token.

/**
 * Test Ajax JSON Response with CSRF Token
 * Example uses a `post` entity
 *
 * The PHP code returns `return new JsonResponse(true, 200);`
 */
public function testAjaxJsonResponse()
{
    // Form fields (make sure they pass validation!)
    $form['post']['title'] = 'test title';
    $form['post']['content'] = 'test content';

    // Create our CSRF token - with $intention = `post_type`
    $csrfToken = $client->getContainer()->get('form.csrf_provider')->generateCsrfToken('post_type');
    $form['post']['_token'] = $csrfToken; // Add it to your `csrf_field_name`

    // Simulate the ajax request
    $client->request('POST', '/path/to/ajax/', $form, array(), array(
        'HTTP_X-Requested-With' => 'XMLHttpRequest',
    ));

    // Test we get a valid JSON response
    $response = $client->getResponse();
    $this->assertSame(200, $client->getResponse()->getStatusCode());
    $this->assertSame('application/json', $response->headers->get('Content-Type'));

    // Assert the content
    $this->assertEquals('true', $response->getContent());
    $this->assertNotEmpty($client->getResponse()->getContent());
}
Sign up to request clarification or add additional context in comments.

1 Comment

Note that as of Symfony 2.3 the service name is security.csrf.token_manager instead of form.csrf_provider.
2

While this question is very old, it still pops up as first result on Google, so I'd though I'd update it with my findings using Symfony 5.4 / 6.x.


Short answer: use the result of your Form type's getBlockPrefix() method as the tokenId:

$csrfToken = self::getContainer()->get('security.csrf.token_manager')->getToken('your_blockprefix');

Long answer:

This is the place where Symfony creates the CSRF Token within it's form system: https://github.com/symfony/symfony/blob/6.3/src/Symfony/Component/Form/Extension/Csrf/Type/FormTypeCsrfExtension.php#L81

The tokenId will be determined in the following order:

  • the form type's option csrf_token_id if present
  • the form type's block prefix as returned by the getBlockPrefix() method if present (see documentation)
  • the Entity's class name, converted to lowercase with underscores (see source)

Since I didn't want to add the csrf_token_id option to every single Form Type, I wrote the following method to obtain the CSRF Token based on the fully qualified name of a Form Type:

protected function generateCSRFToken(string $formTypeFQN): string
{
    $reflectionClass = new \ReflectionClass($formTypeFQN);
    $constructor = $reflectionClass->getConstructor();
    $args = [];
    foreach ($constructor->getParameters() as $parameter) {
        $args[] = $this->createMock($parameter->getType()->getName());
    }
    /** @var FormTypeInterface $instance */
    $instance = $reflectionClass->newInstance(... $args);
    return self::getContainer()->get('security.csrf.token_manager')->getToken($instance->getBlockPrefix());
}

It instantiates an object of the Form Type mocking every required constructor parameter and then creates a CSRF token based on the instance's block prefix.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.