Security
Warning: You are browsing the documentation for Symfony 3.x, which is no longer maintained.
Read the updated version of this page for Symfony 7.1 (the current stable version).
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
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. If you have separated web and API sections on your site, you will need more firewall entries. 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
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.
Note
Argon2i is the hashing algorithm as
recommended by industry standards, but this won't be available to you unless
you are using PHP 7.2+ or have the libsodium extension installed.
bcrypt
is sufficient for most applications.
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
- 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
- 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. Placing it above each action makes it consistent and readable.
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\Security;
use Symfony\Component\Routing\Annotation\Route;
// ...
/**
* 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\Security;
use Symfony\Component\Routing\Annotation\Route;
/**
* @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 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 %}
A good solution - if your logic is simple enough - can be 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 12
use AppBundle\Entity\Post;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\Routing\Annotation\Route;
/**
* @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 27
/**
* @Route("/{id}/edit", name="admin_post_edit")
*/
public function editAction($id)
{
$post = $this->getDoctrine()
->getRepository(Post::class)
->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 AppBundle\Entity\Post;
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;
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, [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, ['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.
Next: Web Assets