Configuration In Security.yml


In the previous video we created a basic login form using a Bootstrap sign in template. We added a SecurityController which enabled our site visitors to hit the /login route and see this new login form.

However, if the user were to fill the form in, and then click 'Sign in' then unfortunately, not a lot would happen.

This is what we are going to fix in this video.

To help us get started as easily as possible we are going to create a 'hardcoded' user which will reside in our Symfony site configuration. This means we won't need to store off our user details to the database at this stage. Instead this user will be created in Symfony's security.yml file.

By default we start with the following security.yml file:

# /app/config/security.yml

security:
    providers:
        in_memory:
            memory: ~

    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            anonymous: ~

I've stripped out all the comments from this file. The section we are interested in is under security.providers.in_memory. Let's change this up:

# /app/config/security.yml

security:
    providers:
        in_memory:
            memory:
                users:
                    admin:
                        password:
                        roles: 'ROLE_ADMIN'

    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            anonymous: ~

Ok, so we've defined a new key under security.providers.in_memory.memory called users.

This users key contains on user, the admin user.

We could add more in here, just add in another entry at the same level of indentation as the admin user. For now, however, we only need the one.

I've left the password value blank, and that's intentional. More on this in a moment.

We also have defined the role of ROLE_ADMIN. When our admin user is successfully logged in, this user will get given this role. We could add any entries we like here. This roles property accepts either a string, or an array. For now, we have simply passed in a single string.

Ok, so back to the password.

Currently it's not valid - it's not blank, it's simply empty.

To make this work, we could either configure plain text passwords - don't do this - or create an encoded password.

We're going to go with the encoded password. I strongly urge you not to use plaintext passwords for two reasons:

  • passwords should never be in plain text - it's a bad habit to get into
  • I saw this being used for real, in real live production site in the financial services industry... I kid you not.

The thing is, most developers are lazy. I know I am. We all seem to have good intentions, but terrible memories. And once things work, we tend to immediately forget about changing that little 'hack' we used because heck, it works and we have 999 other things that don't. So, let's move on, right?

Anyway, we need to generate an encoded password for our password key / value pair.

To do this, fortunately, Symfony comes with a built-in command line utility which we can make use of:

php bin/console security:encode-password

Symfony Password Encoder Utility
================================

  [RuntimeException]
  No encoder has been configured for account "Symfony\Component\Security\Core\User\User".

security:encode-password [--empty-salt] [-h|--help] [-q|--quiet] [-v|vv|vvv|--verbose] [-V|--version] [--ansi] [--no-ansi] [-n|--no-interaction] [-e|--env ENV] [--no-debug] [--] <command> [<password>] [<user-class>]

Ok, so on the command line the output looks a little prettier than this.

The error message here is actually really useful:

No encoder has been configured for account "Symfony\Component\Security\Core\User\User".

We have no encoder configured.

Also, notice that Symfony considers our in_memory user an instance of Symfony\Component\Security\Core\User\User. We will need to know this as we can change up the encoding method on a per user type basis. Don't worry about this for now, we will cover it when we get to adding Database-backed users.

Let's add in an encoder now:

# /app/config/security.yml

security:

    encoders:
        Symfony\Component\Security\Core\User\User:
            algorithm: bcrypt

    providers:
        in_memory:
            memory:
                users:
                    admin:
                        password:
                        roles: 'ROLE_ADMIN'

    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            anonymous: ~

We're going to use bcrypt for encoding passwords.

With this value in place, we can retry our call the security:encode-password, and this time we should get a little further:

php bin/console security:encode-password

Symfony Password Encoder Utility
================================

 Type in your password to be encoded:
 > # you won't see anything here, but I typed: admin

 ------------------ ---------------------------------------------------------------
  Key                Value
 ------------------ ---------------------------------------------------------------
  Encoder used       Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder
  Encoded password   $2y$13$C3D/lnwWeh73axMnldcB.euo.Gkv4IThttEFp2.yaEWiIt585zbOa
 ------------------ ---------------------------------------------------------------

 ! [NOTE] Bcrypt encoder used: the encoder generated its own built-in salt.

 [OK] Password encoding succeeded

Cool. We have encoded our password. Now, we simply need to copy this value back into the security.yml config:

# /app/config/security.yml

security:

    encoders:
        Symfony\Component\Security\Core\User\User:
            algorithm: bcrypt

    providers:
        in_memory:
            memory:
                users:
                    admin:
                        password: $2y$13$C3D/lnwWeh73axMnldcB.euo.Gkv4IThttEFp2.yaEWiIt585zbOa #here
                        roles: 'ROLE_ADMIN'

    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            anonymous: ~

Perfect.

There's a few extra things to note here.

Firstly, you can use shorthand when typing in Symfony console commands - no need to type out the full thing every time. You just need something unique enough to identify any particular command, e.g.:

php bin/console security:enc

Secondly, you can pass in the password as part of the console command - or if you omit it, it will go into the interactive mode as shown above.

If you are going to pass in your password via the command line, remember that if you start your Bash line entry with a space then it won't be saved off to the history command output. Probably safest just to use the interactive mode all the same.

Lastly, one of the nice things about using bcrypt is that we don't need to worry about a salt.

Functioning Form

With a user to use for testing we can go ahead and finish up the Sign In form integration.

To begin with, we need to explicitly set up the form's action path. Remember that as we are rendering these form fields out completely by hand, Symfony isn't in control of any of this. We need to be as specific as possible to ensure this all works the way we expect it too.

<!-- /app/Resources/views/security/login.html.twig -->

{% extends 'base.html.twig' %}

{% block body %}
    <form class="form-signin" action="{{ path('login') }}" method="POST">
        <h2 class="form-signin-heading">Please sign in</h2>
        <label for="_username" class="sr-only">Username</label>
        <input type="text"
               id="_username"
               name="_username"
               class="form-control"
               placeholder="Username"
               required
               autofocus>
        <label for="_password" class="sr-only">Password</label>
        <input type="password"
               id="_password"
               name="_password"
               class="form-control"
               placeholder="Password"
               required>
        <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
    </form>
{% endblock %}

There's three important changes here.

Firstly, we've explicitly set the action and method:

<form class="form-signin" action="{{ path('login') }}" method="POST">

We're using the build in Twig path function to ensure we are always pointing the login route, which we defined in our SecurityController.

We've set the method to POST, as this is always going to be a POST request.

Next, we've added a name property to both of our form input fields:

These two name properties must be in the format _username for our username, and ahem, _password for our password :)

When I first started working with Symfony's login system, I distinctly remember missing out on this rather crucial part and subsequently lost a good few hours in the resulting confusion.

If you really don't want _username and _password, you can change them:

# /app/config/security.yml

security:

    // * snip *

    firewalls:
        main:
            username_parameter: '_custom_username'
            password_parameter: '_custom_password'
            anonymous: ~

If you do this, be sure to use the same value as your name parameter in your login template. Starting with an underscore is not mandatory, it's just convention.

We're not quite done just yet.

Next, we must update our application's firewall.

Now, a firewall in Symfony is not quite the same as iptables or some enterprise hardware firewall that retails for a rather cheap £42,000. I mean, for a start it's free. Well, open source.

Anyway, I digress.

A firewall in Symfony allows us as developers to configure authentication methods.

This is an incredibly flexible arrangement, which inevitably means it can also be incredibly confusing to begin with.

Let's start with what we have already, and then build on it.

# /app/config/security.yml

security:

    // * snip *

    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            anonymous: ~

Firstly, ordering matters.

We have dev before main.

If any of the patterns inside dev match, then main will never be hit.

This pattern key takes a regular expression. Regular expressions are a pain to learn and understand, but are both incredibly useful and conveniently, a lot more easy to decipher than they used to be, thanks to online tools like regex101.com:

symfony-dev-firewall-regex-explained

Anyway, if we match any of the configuration in the dev firewall then security is false (or disabled to you and me), so we can effectively ignore them for the purposes of this tutorial.

As it stands our main firewall is configured just enough to stop Symfony throwing a massive Benny. But aside from that, we need to start working again here.

We'll start off by defining our own pattern.

We want to match any other URI that's not covered by dev. Thankfully, regex has us covered here:

# /app/config/security.yml

security:

    // * snip *

    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            pattern: ^/
            anonymous: ~

Literally our new rule says - match anything starting with a /. In other words, if it wasn't matched by dev, we're interested in it.

But then we have anonymous: ~, where ~ is YAML for null. In other words, this means any site visitor will end up being anonymously identified. Huh?

Right, so there is no noticeable difference between a user who is “anonymously authenticated” and an unauthenticated user. This concept is used for authorisation. A firewall is concerned with authentication. Authorisation is not the same as authentication.

Imagine you own a shop. You allow potential customers to come inside your shop. You don't know who they are, but you know they are somebody. This is similar to anonymous authentication.

Then, to go behind your shop counter and operate the till - hopefully - this person is an authorised member of staff.

I hope that makes sense - but do feel free to shout up (by way of a comment below) if at all unsure. It's a tricky subject.

To continue, we are going to describe our login process. For this we will use form_login, and we're going to need two keys - login_path, and check_path configuring underneath form_login to tell Symfony which routes to intercept.

form_login is an authentication provider that comes included with Symfony. You can create custom authentication providers, but your time may be better spent first checking if an existing bundle is available to meet any particular needs you may have.

login_path, and check_path are parts of the form_login configuration.

I use /login for both, but you can change these to be anything you like. For my needs, /login has always worked fine.

We will add in logout: true, which won't immediately work, but will be needed in a forthcoming video.

Finally, we will add in the key of provider, and the value of in_memory. This ensures that when trying to authenticate users for routes matching the given firewall pattern, that these users will be found in the in_memory value under the providers section we configured earlier.

# /app/config/security.yml

security:

    encoders:
        Symfony\Component\Security\Core\User\User:
            algorithm: bcrypt

    providers:
        in_memory:
            memory:
                users:
                    admin:
                        password: $2y$13$C3D/lnwWeh73axMnldcB.euo.Gkv4IThttEFp2.yaEWiIt585zbOa #here
                        roles: 'ROLE_ADMIN'

    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            pattern: ^/
            provider: in_memory
            form_login:
                login_path: login
                check_path: login
            logout: true
            anonymous: ~

At this point you should be able to login with admin / admin.

There's still a few more steps we need to take to make this function more in line with our basic expectations. For example, when logging in currently you will simply be redirected to the /login route after successfully logging in. Kinda odd.

We will continue on with this, and more, in the very next video.

Code For This Course

Get the code for this course.

Episodes