Skip to content

[DX] New service to simplify password encoding #3995

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 1, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 31 additions & 4 deletions book/security.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1459,30 +1459,57 @@ is available by calling the PHP function :phpfunction:`hash_algos`.
Determining the Hashed Password
...............................

.. versionadded:: 2.6
The ``security.password_encoder`` service was introduced in Symfony 2.6.

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();
$plainPassword = 'ryanpass';
$encoded = $this->container->get('security.password_encoder')
->encodePassword($user, $plainPassword);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$planPassword is not defined in this context. Why don't we reuse "ryanpass" as it was before?


$encoder = $factory->getEncoder($user);
$password = $encoder->encodePassword('ryanpass', $user->getSalt());
$user->setPassword($password);
$user->setPassword($encoded);

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``.

.. sidebar:: Get the User Encoder

In some cases, you need a specific encoder for a given user (e.g. ``Acme\UserBundle\Entity\User``).
You can use the ``EncoderFactory`` to get this encoder::

$factory = $this->get('security.encoder_factory');
$user = new Acme\UserBundle\Entity\User();

$encoder = $factory->getEncoder($user);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm mixed on this. Will anyone ever really need to get the security.encoder_factory directly anymore? Even if you need to inject an encoding service into your own service, you could just inject the security.password_encoder directly. It would be easy to mock too.

So, my question to everyone is: is this worth even mentioning? Certainly, in super-advanced cases, someone really smart could find this service if they need it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tend to agree with you. I can't imagine a use case where this is needed. It might add more confusion than clarifying anything if we keep it here.

.. caution::

When you allow a user to submit a plaintext password (e.g. registration
form, change password form), you *must* have validation that guarantees
that the password is 4096 characters or less. Read more details in
:ref:`How to implement a simple Registration Form <cookbook-registration-password-max>`.

Validating a Plaintext Password
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Sometimes you want to check if a plain password is valid for a given user::

// a user instance of some class which implements Symfony\Component\Security\Core\User\UserInterface
$user = ...;

// the password that should be checked
$plainPassword = ...;

$isValidPassword = $this->container->get('security.password_encoder')
->isPasswordValid($user, $plainPassword);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, now that I think about it, I also think that this is an "edge case". But, one use might be if you want the user to type in their old password to change to a new one or something similar.

So let's keep this here. But I think we need to revisit these chapters later and maybe move some stuff around.

Retrieving the User Object
~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
38 changes: 16 additions & 22 deletions cookbook/security/custom_password_authenticator.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@ Imagine you want to allow access to your website only between 2pm and 4pm
UTC. Before Symfony 2.4, you had to create a custom token, factory, listener
and provider. In this entry, you'll learn how to do this for a login form
(i.e. where your user submits their username and password).
Before Symfony 2.6, you had to use the password encoder to authenticate the user password.

The Password Authenticator
--------------------------

.. versionadded:: 2.4
The ``SimpleFormAuthenticatorInterface`` interface was introduced in Symfony 2.4.

.. versionadded:: 2.6
The ``UserPasswordEncoderInterface`` interface was introduced in Symfony 2.6.

First, create a new class that implements
:class:`Symfony\\Component\\Security\\Core\\Authentication\\SimpleFormAuthenticatorInterface`.
Eventually, this will allow you to create custom logic for authenticating
Expand All @@ -27,18 +31,18 @@ the user::
use Symfony\Component\Security\Core\Authentication\SimpleFormAuthenticatorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserProviderInterface;

class TimeAuthenticator implements SimpleFormAuthenticatorInterface
{
private $encoderFactory;
private $encoder;

public function __construct(EncoderFactoryInterface $encoderFactory)
public function __construct(UserPasswordEncoderInterface $encoder)
{
$this->encoderFactory = $encoderFactory;
$this->encoder = $encoder;
}

public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
Expand All @@ -49,12 +53,7 @@ the user::
throw new AuthenticationException('Invalid username or password');
}

$encoder = $this->encoderFactory->getEncoder($user);
$passwordValid = $encoder->isPasswordValid(
$user->getPassword(),
$token->getCredentials(),
$user->getSalt()
);
$passwordValid = $this->encoder->isPasswordValid($user, $token->getCredentials());

if ($passwordValid) {
$currentHour = date('G');
Expand Down Expand Up @@ -127,17 +126,12 @@ Ultimately, your job is to return a *new* token object that is "authenticated"
(i.e. it has at least 1 role set on it) and which has the ``User`` object
inside of it.

Inside this method, an encoder is needed to check the password's validity::
Inside this method, the password encoder is needed to check the password's validity::

$encoder = $this->encoderFactory->getEncoder($user);
$passwordValid = $encoder->isPasswordValid(
$user->getPassword(),
$token->getCredentials(),
$user->getSalt()
);
$passwordValid = $this->encoder->isPasswordValid($user, $token->getCredentials());

This is a service that is already available in Symfony and the password algorithm
is configured in the security configuration (e.g. ``security.yml``) under
This is a service that is already available in Symfony and it uses the password algorithm
that is configured in the security configuration (e.g. ``security.yml``) under
the ``encoders`` key. Below, you'll see how to inject that into the ``TimeAuthenticator``.

.. _cookbook-security-password-authenticator-config:
Expand All @@ -157,7 +151,7 @@ Now, configure your ``TimeAuthenticator`` as a service:

time_authenticator:
class: Acme\HelloBundle\Security\TimeAuthenticator
arguments: ["@security.encoder_factory"]
arguments: ["@security.password_encoder"]

.. code-block:: xml

Expand All @@ -173,7 +167,7 @@ Now, configure your ``TimeAuthenticator`` as a service:
<service id="time_authenticator"
class="Acme\HelloBundle\Security\TimeAuthenticator"
>
<argument type="service" id="security.encoder_factory" />
<argument type="service" id="security.password_encoder" />
</service>
</services>
</container>
Expand All @@ -188,7 +182,7 @@ Now, configure your ``TimeAuthenticator`` as a service:

$container->setDefinition('time_authenticator', new Definition(
'Acme\HelloBundle\Security\TimeAuthenticator',
array(new Reference('security.encoder_factory'))
array(new Reference('security.password_encoder'))
));

Then, activate it in the ``firewalls`` section of the security configuration
Expand Down