Security

Security

Authentication and Firewalls (i.e. Getting the User's Credentials)

You can configure Symfony to authenticate your users using any method you want and to load user information from any source. This is a complex topic, but the Security guide has a lot of information about this.

Regardless of your needs, authentication is configured in security.yml, primarily under the firewalls key.

Best Practice

Best Practice

Unless you have two legitimately different authentication systems and users (e.g. form login for the main site and a token system for your API only), we recommend having only one firewall entry with the anonymous key enabled.

Most applications only have one authentication system and one set of users. For this reason, you only need one firewall entry. There are exceptions of course, especially if you have separated web and API sections on your site. But the point is to keep things simple.

Additionally, you should use the anonymous key under your firewall. If you need to require users to be logged in for different sections of your site (or maybe nearly all sections), use the access_control area.

Best Practice

Best Practice

Use the bcrypt encoder for encoding your users' passwords.

If your users have a password, then we recommend encoding it using the bcrypt encoder, instead of the traditional SHA-512 hashing encoder. The main advantages of bcrypt are the inclusion of a salt value to protect against rainbow table attacks, and its adaptive nature, which allows to make it slower to remain resistant to brute-force search attacks.

With this in mind, here is the authentication setup from our application, which uses a login form to load users from the database:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# app/config/security.yml
security:
    encoders:
        AppBundle\Entity\User: bcrypt

    providers:
        database_users:
            entity: { class: AppBundle:User, property: username }

    firewalls:
        secured_area:
            pattern: ^/
            anonymous: true
            form_login:
                check_path: login
                login_path: login

            logout:
                path: security_logout
                target: homepage

# ... access_control exists, but is not shown here

Tip

The source code for our project contains comments that explain each part.

Authorization (i.e. Denying Access)

Symfony gives you several ways to enforce authorization, including the access_control configuration in security.yml, the @Security annotation and using isGranted on the security.authorization_checker service directly.

Best Practice

Best Practice

  • For protecting broad URL patterns, use access_control;
  • Whenever possible, use the @Security annotation;
  • Check security directly on the security.authorization_checker service whenever you have a more complex situation.

There are also different ways to centralize your authorization logic, like with a custom security voter or with ACL.

Best Practice

Best Practice

  • For fine-grained restrictions, define a custom security voter;
  • For restricting access to any object by any user via an admin interface, use the Symfony ACL.

The @Security Annotation

For controlling access on a controller-by-controller basis, use the @Security annotation whenever possible. It's easy to read and is placed consistently above each action.

In our application, you need the ROLE_ADMIN in order to create a new post. Using @Security, this looks like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
// ...

/**
 * Displays a form to create a new Post entity.
 *
 * @Route("/new", name="admin_post_new")
 * @Security("has_role('ROLE_ADMIN')")
 */
public function newAction()
{
    // ...
}

Using Expressions for Complex Security Restrictions

If your security logic is a little bit more complex, you can use an expression inside @Security. In the following example, a user can only access the controller if their email matches the value returned by the getAuthorEmail() method on the Post object:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
use AppBundle\Entity\Post;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;

/**
 * @Route("/{id}/edit", name="admin_post_edit")
 * @Security("user.getEmail() == post.getAuthorEmail()")
 */
public function editAction(Post $post)
{
    // ...
}

Notice that this requires the use of the ParamConverter, which automatically queries for the Post object and puts it on the $post argument. This is what makes it possible to use the post variable in the expression.

This has one major drawback: an expression in an annotation cannot easily be reused in other parts of the application. Imagine that you want to add a link in a template that will only be seen by authors. Right now you'll need to repeat the expression code using Twig syntax:

1
2
3
{% if app.user and app.user.email == post.authorEmail %}
    <a href=""> ... </a>
{% endif %}

The easiest solution - if your logic is simple enough - is to add a new method to the Post entity that checks if a given user is its author:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// src/AppBundle/Entity/Post.php
// ...

class Post
{
    // ...

    /**
     * Is the given User the author of this Post?
     *
     * @return bool
     */
    public function isAuthor(User $user = null)
    {
        return $user && $user->getEmail() == $this->getAuthorEmail();
    }
}

Now you can reuse this method both in the template and in the security expression:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
use AppBundle\Entity\Post;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;

/**
 * @Route("/{id}/edit", name="admin_post_edit")
 * @Security("post.isAuthor(user)")
 */
public function editAction(Post $post)
{
    // ...
}
1
2
3
{% if post.isAuthor(app.user) %}
    <a href=""> ... </a>
{% endif %}

Checking Permissions without @Security

The above example with @Security only works because we're using the ParamConverter, which gives the expression access to the post variable. If you don't use this, or have some other more advanced use-case, you can always do the same security check in PHP:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
 * @Route("/{id}/edit", name="admin_post_edit")
 */
public function editAction($id, EntityManagerInterface $em)
{
    $post = $em->getRepository('AppBundle:Post')
        ->find($id);

    if (!$post) {
        throw $this->createNotFoundException();
    }

    if (!$post->isAuthor($this->getUser())) {
        $this->denyAccessUnlessGranted('edit', $post);
    }
    // equivalent code without using the "denyAccessUnlessGranted()" shortcut:
    //
    // use Symfony\Component\Security\Core\Exception\AccessDeniedException;
    // ...
    //
    // if (!$this->get('security.authorization_checker')->isGranted('edit', $post)) {
    //    throw $this->createAccessDeniedException();
    // }

    // ...
}

Security Voters

If your security logic is complex and can't be centralized into a method like isAuthor(), you should leverage custom voters. These are an order of magnitude easier than ACLs and will give you the flexibility you need in almost all cases.

First, create a voter class. The following example shows a voter that implements the same getAuthorEmail() logic you used above:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
namespace AppBundle\Security;

use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\User\UserInterface;
use AppBundle\Entity\Post;

class PostVoter extends Voter
{
    const CREATE = 'create';
    const EDIT   = 'edit';

    /**
     * @var AccessDecisionManagerInterface
     */
    private $decisionManager;

    public function __construct(AccessDecisionManagerInterface $decisionManager)
    {
        $this->decisionManager = $decisionManager;
    }

    protected function supports($attribute, $subject)
    {
        if (!in_array($attribute, array(self::CREATE, self::EDIT))) {
            return false;
        }

        if (!$subject instanceof Post) {
            return false;
        }

        return true;
    }

    protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
    {
        $user = $token->getUser();
        /** @var Post */
        $post = $subject; // $subject must be a Post instance, thanks to the supports method

        if (!$user instanceof UserInterface) {
            return false;
        }

        switch ($attribute) {
            case self::CREATE:
                // if the user is an admin, allow them to create new posts
                if ($this->decisionManager->decide($token, array('ROLE_ADMIN'))) {
                    return true;
                }

                break;
            case self::EDIT:
                // if the user is the author of the post, allow them to edit the posts
                if ($user->getEmail() === $post->getAuthorEmail()) {
                    return true;
                }

                break;
        }

        return false;
    }
}

If you're using the default services.yml configuration, your application will autoconfigure your security voter and inject an AccessDecisionManagerInterface instance into it thanks to autowiring.

Now, you can use the voter with the @Security annotation:

1
2
3
4
5
6
7
8
/**
 * @Route("/{id}/edit", name="admin_post_edit")
 * @Security("is_granted('edit', post)")
 */
public function editAction(Post $post)
{
    // ...
}

You can also use this directly with the security.authorization_checker service or via the even easier shortcut in a controller:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
/**
 * @Route("/{id}/edit", name="admin_post_edit")
 */
public function editAction($id)
{
    $post = ...; // query for the post

    $this->denyAccessUnlessGranted('edit', $post);

    // or without the shortcut:
    //
    // use Symfony\Component\Security\Core\Exception\AccessDeniedException;
    // ...
    //
    // if (!$this->get('security.authorization_checker')->isGranted('edit', $post)) {
    //    throw $this->createAccessDeniedException();
    // }
}

Learn More

The FOSUserBundle, developed by the Symfony community, adds support for a database-backed user system in Symfony. It also handles common tasks like user registration and forgotten password functionality.

Enable the Remember Me feature to allow your users to stay logged in for a long period of time.

When providing customer support, sometimes it's necessary to access the application as some other user so that you can reproduce the problem. Symfony provides the ability to impersonate users.

If your company uses a user login method not supported by Symfony, you can develop your own user provider and your own authentication provider.


This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.