diff --git a/book/security.rst b/book/security.rst index 2e7409bdb8a..59432b994d9 100644 --- a/book/security.rst +++ b/book/security.rst @@ -660,7 +660,7 @@ see :doc:`/cookbook/security/form_login`. ), ), - **3. Be sure ``/login_check`` is behind a firewall** + **3. Be sure /login_check is behind a firewall** Next, make sure that your ``check_path`` URL (e.g. ``/login_check``) is behind the firewall you're using for your form login (in this example, @@ -1206,19 +1206,6 @@ custom user class is that it implements the :class:`Symfony\\Component\\Security interface. This means that your concept of a "user" can be anything, as long as it implements this interface. -.. versionadded:: 2.1 - In Symfony 2.1, the ``equals`` method was removed from ``UserInterface``. - If you need to override the default implementation of comparison logic, - implement the new :class:`Symfony\\Component\\Security\\Core\\User\\EquatableInterface` - interface. - -.. note:: - - The user object will be serialized and saved in the session during requests, - therefore it is recommended that you `implement the \Serializable interface`_ - in your user object. This is especially important if your ``User`` class - has a parent class with private properties. - Next, configure an ``entity`` user provider, and point it to your ``User`` class: @@ -1278,7 +1265,7 @@ in plain text (whether those users are stored in a configuration file or in a database somewhere). Of course, in a real application, you'll want to encode your users' passwords for security reasons. This is easily accomplished by mapping your User class to one of several built-in "encoders". For example, -to store your users in memory, but obscure their passwords via ``sha1``, +to store your users in memory, but obscure their passwords via ``bcrypt``, do the following: .. configuration-block:: @@ -1292,14 +1279,17 @@ do the following: in_memory: memory: users: - ryan: { password: bb87a29949f3a1ee0559f8a57357487151281386, roles: 'ROLE_USER' } - admin: { password: 74913f5cd5f61ec0bcfdb775414c2fb3d161b620, roles: 'ROLE_ADMIN' } + ryan: + password: $2a$12$w/aHvnC/XNeDVrrl65b3dept8QcKqpADxUlbraVXXsC03Jam5hvoO + roles: 'ROLE_USER' + admin: + password: $2a$12$HmOsqRDJK0HuMDQ5Fb2.AOLMQHyNHGD0seyjU3lEVusjT72QQEIpW + roles: 'ROLE_ADMIN' encoders: Symfony\Component\Security\Core\User\User: - algorithm: sha1 - iterations: 1 - encode_as_base64: false + algorithm: bcrypt + cost: 12 .. code-block:: xml @@ -1309,18 +1299,18 @@ do the following: + algorithm="bcrypt" + cost="12" + /> .. code-block:: php @@ -1333,11 +1323,11 @@ do the following: 'memory' => array( 'users' => array( 'ryan' => array( - 'password' => 'bb87a29949f3a1ee0559f8a57357487151281386', + 'password' => '$2a$12$w/aHvnC/XNeDVrrl65b3dept8QcKqpADxUlbraVXXsC03Jam5hvoO', 'roles' => 'ROLE_USER', ), 'admin' => array( - 'password' => '74913f5cd5f61ec0bcfdb775414c2fb3d161b620', + 'password' => '$2a$12$HmOsqRDJK0HuMDQ5Fb2.AOLMQHyNHGD0seyjU3lEVusjT72QQEIpW', 'roles' => 'ROLE_ADMIN', ), ), @@ -1346,77 +1336,36 @@ do the following: ), 'encoders' => array( 'Symfony\Component\Security\Core\User\User' => array( - 'algorithm' => 'sha1', - 'iterations' => 1, - 'encode_as_base64' => false, + 'algorithm' => 'bcrypt', + 'iterations' => 12, ), ), )); -By setting the ``iterations`` to ``1`` and the ``encode_as_base64`` to false, -the password is simply run through the ``sha1`` algorithm one time and without -any extra encoding. You can now calculate the hashed password either programmatically -(e.g. ``hash('sha1', 'ryanpass')``) or via some online tool like `functions-online.com`_ - -.. tip:: - - Supported algorithms for this method depend on your PHP version. - A full list is available calling the PHP function :phpfunction:`hash_algos`. - -If you're creating your users dynamically (and storing them in a database), -you can use even tougher hashing algorithms and then rely on an actual password -encoder object to help you encode passwords. For example, suppose your User -object is ``Acme\UserBundle\Entity\User`` (like in the above example). First, -configure the encoder for that user: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - # ... - - encoders: - Acme\UserBundle\Entity\User: sha512 - - .. code-block:: xml - - - - - - - +.. versionadded:: 2.2 + The BCrypt encoder was introduced in Symfony 2.2. - .. code-block:: php +You can now calculate the hashed password either programmatically +(e.g. ``password_hash('ryanpass', PASSWORD_BCRYPT, array('cost' => 12));``) +or via some online tool. - // app/config/security.php - $container->loadFromExtension('security', array( - // ... - 'encoders' => array( - 'Acme\UserBundle\Entity\User' => 'sha512', - ), - )); +.. include:: /cookbook/security/_ircmaxwell_password-compat.rst.inc -In this case, you're using the stronger ``sha512`` algorithm. Also, since -you've simply specified the algorithm (``sha512``) as a string, the system -will default to hashing your password 5000 times in a row and then encoding -it as base64. In other words, the password has been greatly obfuscated so -that the hashed password can't be decoded (i.e. you can't determine the password -from the hashed password). +Supported algorithms for this method depend on your PHP version. A full list +is available by calling the PHP function :phpfunction:`hash_algos`. .. versionadded:: 2.2 As of Symfony 2.2 you can also use the :ref:`PBKDF2 ` - and :ref:`BCrypt ` password encoders. + password encoder. Determining the Hashed Password ............................... -If you have some sort of registration form for users, you'll need to be able -to determine the hashed password so that you can set it on your user. No -matter what algorithm you configure for your user object, the hashed password -can always be determined in the following way from a controller:: +If you're storing users in the database and you have some sort of registration +form for users, you'll need to be able to determine the hashed password so +that you can set it on your user before inserting it. No matter what algorithm +you configure for your user object, the hashed password can always be determined +in the following way from a controller:: $factory = $this->get('security.encoder_factory'); $user = new Acme\UserBundle\Entity\User(); @@ -1425,6 +1374,10 @@ can always be determined in the following way from a controller:: $password = $encoder->encodePassword('ryanpass', $user->getSalt()); $user->setPassword($password); +In order for this to work, just make sure that you have the encoder for your +user class (e.g. ``Acme\UserBundle\Entity\User``) configured under the ``encoders`` +key in ``app/config/security.yml``. + .. caution:: When you allow a user to submit a plaintext password (e.g. registration @@ -2077,5 +2030,4 @@ Learn more from the Cookbook .. _`JMSSecurityExtraBundle`: http://jmsyst.com/bundles/JMSSecurityExtraBundle/1.2 .. _`FOSUserBundle`: https://github.com/FriendsOfSymfony/FOSUserBundle .. _`implement the \Serializable interface`: http://php.net/manual/en/class.serializable.php -.. _`functions-online.com`: http://www.functions-online.com/sha1.html .. _`Timing attack`: http://en.wikipedia.org/wiki/Timing_attack diff --git a/cookbook/security/_ircmaxwell_password-compat.rst.inc b/cookbook/security/_ircmaxwell_password-compat.rst.inc new file mode 100644 index 00000000000..3f96c454488 --- /dev/null +++ b/cookbook/security/_ircmaxwell_password-compat.rst.inc @@ -0,0 +1,13 @@ +.. caution:: + + If you're using PHP 5.4 or lower, you'll need to install the ``ircmaxell/password-compat`` + library via Composer in order to be able to use the ``bcrypt`` encoder: + + .. code-block:: json + + { + "require": { + ... + "ircmaxell/password-compat": "~1.0.3" + } + } diff --git a/cookbook/security/entity_provider.rst b/cookbook/security/entity_provider.rst index b412c878c9f..b6919839b83 100644 --- a/cookbook/security/entity_provider.rst +++ b/cookbook/security/entity_provider.rst @@ -32,7 +32,7 @@ The Data Model -------------- For the purpose of this cookbook, the ``AcmeUserBundle`` bundle contains a -``User`` entity class with the following fields: ``id``, ``username``, ``salt``, +``User`` entity class with the following fields: ``id``, ``username``, ``password``, ``email`` and ``isActive``. The ``isActive`` field tells whether or not the user account is active. @@ -77,11 +77,6 @@ focus on the most important methods that come from the */ private $username; - /** - * @ORM\Column(type="string", length=32) - */ - private $salt; - /** * @ORM\Column(type="string", length=64) */ @@ -100,7 +95,8 @@ focus on the most important methods that come from the public function __construct() { $this->isActive = true; - $this->salt = md5(uniqid(null, true)); + // may not be needed, see section on salt below + // $this->salt = md5(uniqid(null, true)); } /** @@ -116,7 +112,9 @@ focus on the most important methods that come from the */ public function getSalt() { - return $this->salt; + // you *may* need a real salt depending on your encoder + // see section on salt below + return null; } /** @@ -150,8 +148,9 @@ focus on the most important methods that come from the return serialize(array( $this->id, $this->username, - $this->salt, $this->password, + // see section on salt below + // $this->salt, )); } @@ -163,19 +162,13 @@ focus on the most important methods that come from the list ( $this->id, $this->username, - $this->salt, $this->password, + // see section on salt below + // $this->salt ) = unserialize($serialized); } } -.. note:: - - If you choose to implement - :class:`Symfony\\Component\\Security\\Core\\User\\EquatableInterface`, - you determine yourself which properties need to be compared to distinguish - your user objects. - .. tip:: :ref:`Generate the database table ` @@ -198,27 +191,9 @@ interface forces the class to implement the five following methods: For more details on each of these, see :class:`Symfony\\Component\\Security\\Core\\User\\UserInterface`. -.. sidebar:: What is the importance of serialize and unserialize? - - The :phpclass:`Serializable` interface and its ``serialize`` and ``unserialize`` - methods have been added to allow the ``User`` class to be serialized - to the session. This may or may not be needed depending on your setup, - but it's probably a good idea. The ``id`` is the most important value - that needs to be serialized because the - :method:`Symfony\\Bridge\\Doctrine\\Security\\User\\EntityUserProvider::refreshUser` - method reloads the user on each request by using the ``id``. In practice, - this means that the User object is reloaded from the database on each - request using the ``id`` from the serialized object. This makes sure - all of the User's data is fresh. - - Symfony also uses the ``username``, ``salt``, and ``password`` to verify - that the User has not changed between requests. Failing to serialize - these may cause you to be logged out on each request. If your User implements - :class:`Symfony\\Component\\Security\\Core\\User\\EquatableInterface`, - then instead of these properties being checked, your ``isEqualTo`` method - is simply called, and you can check whatever properties you want. Unless - you understand this, you probably *won't* need to implement this interface - or worry about it. +If you're curious about the ``serialize`` method or are looking for details +on the logic of seeing if the User stored in the session is the same as the +one stored in the database, see :ref:`cookbook-security-serialize-equatable`. Below is an export of the ``User`` table from MySQL with user ``admin`` and password ``admin`` (which has been encoded). For details on how to create @@ -227,16 +202,30 @@ user records and encode their password, see :ref:`book-security-encoding-user-pa .. code-block:: bash $ mysql> select * from acme_users; - +----+----------+------+------------------------------------------+--------------------+-----------+ - | id | username | salt | password | email | is_active | - +----+----------+------+------------------------------------------+--------------------+-----------+ - | 1 | admin | | d033e22ae348aeb5660fc2140aec35850c4da997 | admin@example.com | 1 | - +----+----------+------+------------------------------------------+--------------------+-----------+ + +----+----------+------------------------------------------+--------------------+-----------+ + | id | username | password | email | is_active | + +----+----------+------------------------------------------+--------------------+-----------+ + | 1 | admin | d033e22ae348aeb5660fc2140aec35850c4da997 | admin@example.com | 1 | + +----+----------+------------------------------------------+--------------------+-----------+ The next part will focus on how to authenticate one of these users thanks to the Doctrine entity user provider and a couple of lines of configuration. +.. sidebar:: Do you need to use a Salt? + + Yes. Hashing a password with a salt is a necessary step so that encoded + passwords can't be decoded. However, some encoders - like Bcrypt - have + a built-in salt mechanism. If you configure ``bcrypt`` as your encoder + in ``security.yml`` (see the next section), then ``getSalt()`` should + return ``null``, so that Bcrypt generates the salt itself. + + However, if you use an encoder that does *not* have a built-in salting + ability (e.g. ``sha512``), you *must* (from a security perspective) generate + your own, random salt, store it on a ``salt`` property that is saved to + the database, and return it from ``getSalt()``. Some of the code needed + is commented out in the above example. + Authenticating Someone against a Database ----------------------------------------- @@ -257,9 +246,7 @@ then be checked against your User entity records in the database: security: encoders: Acme\UserBundle\Entity\User: - algorithm: sha1 - encode_as_base64: false - iterations: 1 + algorithm: bcrypt role_hierarchy: ROLE_ADMIN: ROLE_USER @@ -282,9 +269,7 @@ then be checked against your User entity records in the database: ROLE_USER @@ -307,9 +292,7 @@ then be checked against your User entity records in the database: $container->loadFromExtension('security', array( 'encoders' => array( 'Acme\UserBundle\Entity\User' => array( - 'algorithm' => 'sha1', - 'encode_as_base64' => false, - 'iterations' => 1, + 'algorithm' => 'bcrypt', ), ), 'role_hierarchy' => array( @@ -335,12 +318,14 @@ then be checked against your User entity records in the database: ), )); -The ``encoders`` section associates the ``sha1`` password encoder to the entity +The ``encoders`` section associates the ``bcrypt`` password encoder to the entity class. This means that Symfony will expect the password that's stored in -the database to be encoded using this algorithm. For details on how to create +the database to be encoded using this encoder. For details on how to create a new User object with a properly encoded password, see the :ref:`book-security-encoding-user-password` section of the security chapter. +.. include:: /cookbook/security/_ircmaxwell_password-compat.rst.inc + The ``providers`` section defines an ``administrators`` user provider. A user provider is a "source" of where users are loaded during authentication. In this case, the ``entity`` keyword means that Symfony will use the Doctrine @@ -749,3 +734,45 @@ fetch the user and their associated roles with a single query:: The ``QueryBuilder::leftJoin()`` method joins and fetches related roles from the ``AcmeUserBundle:User`` model class when a user is retrieved by their email address or username. + +.. _`cookbook-security-serialize-equatable`: + +Understanding serialize and how a User is Saved in the Session +-------------------------------------------------------------- + +If you're curious about the importance of the ``serialize()`` method inside +the ``User`` class or how the User object is serialized or deserialized, then +this section is for you. If not, feel free to skip this. + +Once the user is logged in, the entire User object is serialized into the +session. On the next request, the User object is deserialized. Then, value +of the ``id`` property is used to re-query for a fresh User object from the +database. Finally, the fresh User object is compared in some way to the deserialized +User object to make sure that they represent the same user. For example, if +the ``username`` on the 2 User objects doesn't match for some reason, then +the user will be logged out for security reasons. + +Even though this all happens automatically, there are a few important side-effects. + +First, the :phpclass:`Serializable` interface and its ``serialize`` and ``unserialize`` +methods have been added to allow the ``User`` class to be serialized +to the session. This may or may not be needed depending on your setup, +but it's probably a good idea. In theory, only the ``id`` needs to be serialized, +because the :method:`Symfony\\Bridge\\Doctrine\\Security\\User\\EntityUserProvider::refreshUser` +method refreshes the user on each request by using the ``id`` (as explained +above). However in practice, this means that the User object is reloaded from +the database on each request using the ``id`` from the serialized object. +This makes sure all of the User's data is fresh. + +Symfony also uses the ``username``, ``salt``, and ``password`` to verify +that the User has not changed between requests. Failing to serialize +these may cause you to be logged out on each request. If your User implements +the :class:`Symfony\\Component\\Security\\Core\\User\\EquatableInterface`, +then instead of these properties being checked, your ``isEqualTo`` method +is simply called, and you can check whatever properties you want. Unless +you understand this, you probably *won't* need to implement this interface +or worry about it. + +.. versionadded:: 2.1 + In Symfony 2.1, the ``equals`` method was removed from ``UserInterface`` + and the ``EquatableInterface`` was added in its place.