Posted by Rainer Kraft on Sep 25, 2018

Pride comes before the fall

Symfony’s long term support for v2.8 ends in November. Eek! Quite a substantial part of our codebase relies heavily on 2.8 and some of it can’t be updated yet.

We can’t yet make the jump to v4, so we settled on upgrading Symfony to v3.4. In this short article I’ll describe how I stumbled over a completely unexpected problem with overwriting a standard password encoder used in user/password authentication.

To make a start with upgrading I chose to update the admin part of our site as it is non-public. We’d previously fixed most of the deprecation warnings and therefore the task wasn’t in fact that difficult. A few of our external and internal packages simply had to allow Symfony 3 in their respective composer version constraints, and some had unfixed deprecation issues that needed to be fixed.

I sailed through the task; already daydreaming of basking in praise from my colleagues. These delusions of grandeur are usually a good indicator of imminent failure, and this time was no exception.

Original problem

As part of our login functionality we needed to overwrite Symfony’s BasePasswordEncoder::mergePasswordAndSalt() to merge password and salt in a different way.

Original solution

In 2.8 this is easily done by overriding one of a bunch of parameters in our app’s own SecurityBundle to point to our own class that extended the original encoder method mergePasswordAndSalt().

<parameters>
    <parameter key="security.encoder.digest.class">Our\Own\Implementation\MessageDigestPasswordEncoder</parameter>
</parameters>
use Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder as BaseMessageDigestPasswordEncoder;

class MessageDigestPasswordEncoder extends BaseMessageDigestPasswordEncoder
{
    /**
     * {@inheritdoc}
     */
    protected function mergePasswordAndSalt($password, $salt)
    {
        // ... do your own thang here
    }

Underneath encoders in app/config/security.yml we’d provided previously our user class, AdminUser:

security:
    encoders:
        OurApp\Bundle\SecurityBundle\AdminUser\AdminUser:
            algorithm: some_algorithm
            iterations: 1
            encode_as_base64: false

The parameters here are intended for the actual Encoder class.

New problem

After upgrading to Symfony v3.4 and fixing some minor problems around deprecations, I started to do some manual testing of our admin system. All went well until I tried to login again (I was still logged in from a previous session): I kept getting the error Bad credentials.

It took me a while to realise that in v3.x these parameters (e.g <parameter key="security.encoder.digest.class">) were removed and could no longer be overwritten. Therefore the result of merging the password and salt was now returned by BasePasswordEncoder::mergePasswordAndSalt(), and the hashes didn’t match anymore.

New solution

I started by looking through the documentation - as you do - but didn’t come up with much. Googling the problem also didn’t yield much more information other than some older posts outlining the original solution.

After a lot of trial and error I found the solution, which, of course, is very simple. There is just not one single point in the official documentation where this is explained.

Instead of overwriting the parameter key security.encoder.digest.class we need to make our own extension of the MessageDigestPasswordEncoder class into a service, and pass in the parameters that we previously defined in app/config/security.yml:

<service id="our_app.password.encoder" class="Our\Own\Implementation\MessageDigestPasswordEncoder">
    <argument>some_algorithm</argument>
    <argument>false</argument>
    <argument>1</argument>
</service>

In app/config/security.yml we replace our old encoders entry with:

security:
    encoders:
        our_app_password_encoder:
            id:  our_app.password.encoder

Now we simply have to tell our User class which encoder we want to use, identified by the encoder key in app/config/security.yml. For that we need to implement EncoderAwareInterface:

class AdminUser implements UserInterface, EncoderAwareInterface
{
    // other code
    // ....

    /**
     * Gets the name of the encoder used to encode the password.
     *
     * If the method returns null, the standard way to retrieve the encoder
     * will be used instead.
     *
     * @return string
     */
    public function getEncoderName()
    {
        return 'our_app_password_encoder';
    }
}

That’s all. And I only spent a day and a half on it.

Jobs at MyBuilder and Instapro

We need experienced software engineers who love their craft and want to share their hard-earned knowledge.

View vacancies
comments powered by Disqus