When working with Symfony2, you already have many of the finest tools for securing your web application. There are cases however that require you to add that extra bit. In this post I will point you to the right extension points within a Symfony2 project (or any other project which uses the Security Component for that matter).
Install NelmioSecurityBundle
See the README of the NelmioSecurityBundle. It contains many add-ons for your project, to sign/encrypt cookies, force SSL, prevent clickjacking and prevent untrusted redirects.
Log Security Information
When anything noteworthy related to security happens inside your application, log it to the "security" channel of the application logger. This way you can easily filter your logs in case of a security emergency. When injecting the logger into a service, do it like this:
class SecurityRelatedClass
{
private $logger;
public function __construct(LoggerInterface $logger = null)
{
// there may be no logger defined, depending on your site's configuration
$this->logger = $logger;
}
public function doSomething()
{
if ($this->logger instanceof LoggerInterface) {
// don't make it an error, since there is no failure of the system here
$this->logger->info('Interesting security-related information');
}
// ...
}
}
In the service definition add a tag and mention the channel:
<service id="matthias_security.security_related"
class="Matthias\ApplicationBundle\SecurityRelatedClass">
<argument type="service" id="logger" on-invalid="null" />
<tag name="monolog.logger" channel="security" />
</service>
Enhance CSRF Protection
Each form by default has a "_token" field containing a token that is unique per user since it is based on the session ID. Usually it is the same for all forms of your application. But it is possible to make it different for each form. Use the "intention" option for form types:
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class RegisterType extends AbstractType
{
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'intention' => 'register'
));
}
}
Use a Real Form Type for the Login Form
I don't know why the Symfony documentation tells you to circumvent the entire Form Component and just write your login form in plain and simple HTML. I think it is much better to create your own form type, register it as a service and then render your form in a Twig template, using {{ form_widget(form) }}
, just like any other form. This will (almost) automatically give you the benefit of a standard CSRF token. To make it work, you will also have to adapt your login form type's options and the configuration for each "form_login" section in security.yml
:
security:
firewalls:
my_firewall:
pattern: ^/
form_login:
username_parameter: "login[username]"
password_parameter: "login[password]"
csrf_parameter: "login[_token]"
csrf_provider: form.csrf_provider
intention: authentication
As you can see, we need to provide the name of the form fields containing the username, password and CSRF token. Make sure the intention in the form type matches the one in the security configuration:
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class LoginType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('username', 'text')
->add('password', 'password');
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'intention' => 'authentication'
));
}
public function getName()
{
return 'login';
}
}
In part II: session settings, session invalidation and keeping track of failed login attempts.
Add this to your LoginType class:
private $targetUrl;
public function __construct($targetUrl = '')
{
$this->targetUrl = $targetUrl;
}
...and a setAction to the form builder:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->setAction($this->targetUrl)
->add('username', 'text')
...
Inject the login_check route into the service:
(in YAML):
arguments: [ @=service('router').generate('login_check') ]
(in XML):
<argument type="expression">service('router').generate('login_check')</argument>
As defined in your routing:
login_check:
path: /login_check
Greate article
U saved my day
Thank u
I'm a little confused, doesn't Symfony generate a unique CSRF token by default per form? I could be mistaken but after taking a look at my own project is seems like when you don't specify an intention each form receives a unique CSRF token. The intention option is interesting, are there are areas/uses other than form_login where you can use intention in a similar way?
This has likely changed in the meantime! If you see different CSRF tokens for different forms, than everything is okay now :)
Thank you man. Finally. I spent hours figuring out why login did not work. That part of using just simple twig html for log in form is stupid. And they never said in the documentation how to configure security.yml file properly like you did:
form_login:
username_parameter: "login[username]"
password_parameter: "login[password]"
csrf_parameter: "login[_token]"
csrf_provider: form.csrf_provider
intention: authentication
Thanks alot for that. The docs about log in should be definitevely updated by Symfony guys. And I mean it. In current condition is unusable for newcomers.
Thanks again.
Actually i wrote a component enhancement for this about a year ago, which also will set the error on the form type when displaying. https://github.com/Vandpibe...
Excellent ideas! Thanks for sharing.
Your blog is a gold mine! Thanks for sharing all these valuable information ;)
Thank you too!
Very nice tips!
I've tried a lot to render login form with a Form Type, now i know how to do!
Thanks
Thanks!
The documentation do not use the form component to not intoduce a hard dependency between the form and the security components.
More over the the csrf is not needed on a login form by definition. Of course you can use it to prevent stupid robot to brut force your authentication system. But it is not mandatory.
Great blog post anyway
Thanks Greg. It's a good idea to write separately about the components, but I always thought that "The Book" didn't make this distinction (while "The Components" does). I think it would be good to encourage people to use the Form component whenever they create a form in an application. Any generally implemented form behavior will then also be inherited by the login form.
Also, adding a CSRF-token to the login form will indeed only prevent "stupid" attacks. Once an attacker has done a single attack by hand (i.e. in a browser), he can forge subsequent request with the token received from the first attack. He can than easily automate the following attacks. Symfony does not allow for CSRF-tokens which can only be used once. This would help a lot in slowing down the attacker. See also https://github.com/symfony/... I plan to write about this later.
Greg, why would you not have a CSRF token on a login form? Would you like the attacker to use a network of users to mount a brute force or timing attack which would circumvent any max attempts or time delay defenses?
Yes, use a CSRF - on ALL forms.