Buy

PUT Validation and CSRF Tokens

Validation for newAction(), check! Now let's repeat for updateAction. And that's not much work - we just need to add the whole if (!$form->isValid()) block. I know you hate duplication, so copy the inside of that if statement, head to the bottom of the class, and add a new private function createValidationErrorResponse(). We'll pass it the $form object, and we should type-hint that argument with FormInterface because we're good programmers! Paste the stuff here:

179 lines src/AppBundle/Controller/Api/ProgrammerController.php
... lines 1 - 10
use Symfony\Component\Form\FormInterface;
... lines 12 - 15
class ProgrammerController extends BaseController
{
... lines 18 - 165
private function createValidationErrorResponse(FormInterface $form)
{
$errors = $this->getErrorsFromForm($form);
$data = [
'type' => 'validation_error',
'title' => 'There was a validation error',
'errors' => $errors
];
return new JsonResponse($data, 400);
}
}

Cool! Any time we have a form, we can pass it here and get back a perfectly consistent validation error response. Go back up to newAction() and use this: return $this->createValidationErrorResponse() and pass it the $form object:

179 lines src/AppBundle/Controller/Api/ProgrammerController.php
... lines 1 - 15
class ProgrammerController extends BaseController
{
... lines 18 - 21
public function newAction(Request $request)
{
... lines 24 - 27
if (!$form->isValid()) {
return $this->createValidationErrorResponse($form);
}
... lines 31 - 45
}
... lines 47 - 177
}

Copy those three lines and repeat in updateAction():

179 lines src/AppBundle/Controller/Api/ProgrammerController.php
... lines 1 - 15
class ProgrammerController extends BaseController
{
... lines 18 - 88
public function updateAction($nickname, Request $request)
{
... lines 91 - 101
$form = $this->createForm(new UpdateProgrammerType(), $programmer);
$this->processForm($request, $form);
if (!$form->isValid()) {
return $this->createValidationErrorResponse($form);
}
... lines 108 - 115
}
... lines 117 - 177
}

We could write a test for this, but we've centralized everything so well, that I'm confident that if it works in newAction, it works in updateAction(). Basically, I think that's overkill. But we should re-run our test:

bin/phpunit -c app --filter testValidationErrors

All good. Now run all the tests:

bin/phpunit -c app

Oh! They break immediately! The POST is failing with a 400 response: invalid CSRF token - we saw this a few minutes ago. Every endpoint is failing because we're never sending a CSRF token.

Symfony forms always expect a token. But because we're building a stateless, or session-less API, we don't need CSRF tokens. You would need it if you have a JavaScript frontend that's relying on cookies to authenticate, but you don't need it if your API doesn't store the user in the session.

Let's turn it off. Inside ProgrammerType, in setDefaultOptions() - or configureOptions() if you're on a newer version of Symfony - set csrf_protection to false:

48 lines src/AppBundle/Form/ProgrammerType.php
... lines 1 - 8
class ProgrammerType extends AbstractType
{
... lines 11 - 34
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
... lines 38 - 39
'csrf_protection' => false,
));
}
... lines 43 - 47
}

That'll do it! Try the tests:

bin/phpunit -c app

Back to green! If you're using your form types for HTML pages and on your API, you won't want to set csrf_protection to false inside the class - that'll remove it everywhere. Instead, you can pass csrf_protection in as an option in the third argument to createForm() in your controller. Or you can do something fancier like a Form Type Extension and control this option on a global basis.

FOSRestBundle has an interesting version of this. In the View Layer part of their docs, they show a configuration option that disables CSRF protection based on a role the user has. The idea is that only users that are authenticated via the sessionless-API would have the role you put here. That's a cool idea.

Leave a comment!