We now have a working system to allow administrators to log in and add both Categories, and Wallpapers. They can do all of the needed CRUD operations on these two resources, and as we have seen the more complicated parts (such as removing an uploaded image) are now taken care of.

What we haven't yet done is stop just anybody from accessing the admin panel.

We aren't going to get too complex with our admin security. We will create a single user: "admin", for which we will create a password which will be stored in our security.yml file.

Even though this sounds a little odd, we won't be storing the password in a human readable / plain text format. Instead, we will generate a hashed value which when we attempt to log in, will be compared against the hashed value of whatever password we provide. This is just like how it would work if our passwords were stored in a database. It's just a simpler approach.

With a user created we can then define a firewall configuration, and apply the appropriate access control entries to restrict access to the /admin backend.

Let's start by defining our admin user inside security.yml:

# app/config/security.yml

security:
    providers:
        in_memory:
            memory:
                users:
                    admin:
                        password: ...

Holy nested operations, Batman!

Yeah, the nesting looks weird, especially as we only have one configured security provider - the in_memory provider.

Essentially this is saying that our in_memory provider provides some users (or just one, in our case) from memory. The name "memory" is confusing. We're working from a text file (security.yml) so it's better to think of this as "read from a file provider" :)

Under this memory key we define a list of users.

We only have one, but if you'd like to add more, feel free to do so. Keep the nesting structure, so e.g.:

# app/config/security.yml

security:
    providers:
        in_memory:
            memory:
                users:
                    admin:
                        password: ...
                    dave:
                        password: ...

And so on.

We'll need to set up a valid password. Three dots ain't gunna cut it.

What we need to do is generate an encoded password which represents our plain text password.

Symfony can help us do this. But first we need to define a security encoder which Symfony can then use to generate our passwords for us.

This sounds complicated, and indeed beneath the surface this is venturing into serious boffin level security code that I do not profess to fully understand. One of the primary benefits of using a framework like Symfony is that the security component provides a battle-tested, and security-expert-reviewed set of tools to make sure I don't do something silly (such as roll my own).

Defining an encoder is pretty much just copy paste from the docs:

security:

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

We're using bcrypt here as it is the recommended best algorithm to use by the Symfony docs.

With this small amount of config in place we can create an encoded password. Head over to your terminal:

php bin/console security:encode-password

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

 Type in your password to be encoded:
 >

 ------------------ ---------------------------------------------------------------
  Key                Value
 ------------------ ---------------------------------------------------------------
  Encoder used       Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder
  Encoded password   $2y$13$ibUTDQ6fwHMW3YenrpmygOqgRgOZJp6H8idr9DKA0vhy0OrlagTae
 ------------------ ---------------------------------------------------------------

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


 [OK] Password encoding succeeded

I used the password 'admin' here. You can run this command multiple times entering the same password each time, and each time the generated "Encoded password" will be different.

With the "Encoded password" available to us, we need to update our security.yml file to set this password for our admin user:

security:

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

    providers:
        in_memory:
            memory:
                users:
                    admin:
                        password: $2y$13$ibUTDQ6fwHMW3YenrpmygOqgRgOZJp6H8idr9DKA0vhy0OrlagTae

This is all well and good, but we haven't yet told Symfony that we would like to secure the /admin route.

Before we can do that, we need to tell Symfony when we want to use our in_memory provider configuration.

To do this we need to update the firewalls section in security.yml.

Here's what we have by default:

    firewalls:
        # disables authentication for assets and the profiler, adapt it according to your needs
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false

        main:
            anonymous: ~

We can disregard the dev firewall.

What we care about is the main firewall.

Here's what we are going to use:

    firewalls:
        # disables authentication for assets and the profiler, adapt it according to your needs
        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: ~

There's quite a lot going on here, but rather than cover this here I am going to suggest you watch this video, and read the associated write up where this is covered pretty much identically.

Even with this firewall config in place, we haven't yet told Symfony exactly when to require us to be authenticated. For this, we need to add in some access_control entries:

    access_control:
        - { path: ^/admin/, role: ROLE_ADMIN }
        - { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/, role: IS_AUTHENTICATED_ANONYMOUSLY }

Ok, so what's going on here?

We have three entries. Symfony will read them from top to bottom, and try and match the current route to the logged in user, and their list of roles.

We haven't defined ourselves any roles as of yet. Fortunately, defining roles is really easy - it's just a case of inventing them :)

We can update our in_memory provider entry to ensure our admin user has the expected role:

security:

    role_hierarchy:
        ROLE_ADMIN: [ROLE_USER]


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

    providers:
        in_memory:
            memory:
                users:
                    admin:
                        password: $2y$13$ibUTDQ6fwHMW3YenrpmygOqgRgOZJp6H8idr9DKA0vhy0OrlagTae
                        roles: 'ROLE_ADMIN'

We've added a roles key to our admin user, and specified this user will be given the role of ROLE_ADMIN. All roles start with the text ROLE_, and then whatever comes after we can make up - ROLE_CHEESE, ROLE_MOON, etc. It's entirely dependent on our application what roles we might need.

Again, I don't want to go too deep here as we have covered security roles before.

Note also the inclusion of the role_hierarchy which simply says that any user given the role of ROLE_ADMIN will also gain the rights and privileges of ROLE_USER.

If at all unsure, please do watch this video.

Trying to access the /admin route now produces a different outcome:

Unable to generate a URL for the named route "login" as such route does not exist. 500 Internal Server Error - RouteNotFoundException

Good lord.

Ok, so we expect our users to login, but we haven't set up a login form, or a login route.

To do this we will need a new controller and a route, and we will also need to create a HTML login form.

Fortunately this is pretty much just copy / paste:

<?php

// src/AppBundle/Controller/SecurityController.php

namespace AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;

class SecurityController extends Controller
{
    /**
     * @Route("/login", name="login")
     */
    public function loginAction()
    {
        return $this->render('security/login.html.twig');
    }

    /**
     * @Route("/logout")
     * @throws \RuntimeException
     */
    public function logoutAction()
    {
        throw new \RuntimeException('This should never be called directly.');
    }
}

We told Symfony - via our security.yml entries - to expect us to log in on /login.

Here we define a controller action for that very route.

All we need to do is display a login form when that route is called, and Symfony will (by and large) take care of the rest for us.

One of the most critical parts of the login form is the field name properties.

By default the username field name must be _username, and the password field name must be _password. Symfony will look for these fields by name, and if you get them wrong, you will be redirected back to your login form and it's not super obvious why.

Again, I strongly recommend you watch this video if you'd like to know more as we've already covered this in greater detail.

The template we will render is as follows:

<!-- 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 %}

You may be wondering why our /logout route throws when called. It's a good question. Symfony will intercept calls to this route, but we still must define the controller action to get the route for Symfony to know about it, and be able to intercept it. It's not great in my opinion, but it's how this works, so we have to do it.

And that's about it.

Try browsing the /admin route now and you will be redirected to your login form. Log in with good credentials and you should be good to go. Bad credentials take you back to the login form.

We could enhance this login form further, but that's really outside the scope of this tutorial.

At this point we have achieved all the basic things we set out to do at the start of this series.

There are more topics to cover. There is more work to be done. But right now, it's time for a brew, a rest, and heck, maybe even changing out desktop background to one of these fantastic pictures we've now got in our collection :)


Code For This Course

Get the code for this course.

Share This Episode

If you have found this video helpful, please consider sharing. I really appreciate it.


Episodes in this series

# Title Duration
1 Introduction and Site Demo 02:14
2 Setup and a Basic Wallpaper Gallery 08:43
3 Pagination 08:24
4 Adding a Detail View 04:47
5 Creating a Home Page 11:14
6 Creating our Wallpaper Entity 07:50
7 Wallpaper Setup Command - Part 1 - Symfony Commands As a Service 05:56
8 Wallpaper Setup Command - Part 2 - Injection Is Easy 08:53
9 Wallpaper Setup Command - Part 3 - Doing It With Style 05:37
10 Doctrine Fixtures - Part 1 - Setup and Category Entity Creation 08:52
11 Doctrine Fixtures - Part 2 - Relating Wallpapers with Categories 05:56
12 EasyAdminBundle - Setup and Category Configuration 06:02
13 EasyAdminBundle - Wallpaper Setup and List View 07:46
14 EasyAdminBundle - Starting with Wallpaper Uploads 05:57
15 Testing with PhpSpec to Guide Our Implementation 03:39
16 Using PhpSpec to Test our FileMover 05:34
17 Symfony Dependency Testing with PhpSpec 08:47
18 Defensive Counter Measures 06:32
19 No Tests - Part 1 - Uploading Files in EasyAdminBundle 11:02
20 No Tests - Part 2 - Uploading Files in EasyAdminBundle 07:05
21 Don't Mock What You Don't Own 09:36
22 You've Got To Take The Power Back 07:36
23 Making Symfony Work For Us 08:56
24 Testing The Wallpaper File Path Helper 15:11
25 Finally, It Works! 14:56
26 Why I Prefer Not To Use Twig 16:50
27 Fixing The Fixtures 11:20
28 Untested Updates 14:30
29 Untested Updates Part Two - Now We Can Actually Update 06:33
30 Adding an Image Preview On Edit 12:31
31 Delete Should Remove The Wallpaper Image File 11:02
32 Getting Started Testing Wallpaper Updates 10:02
33 Doing The Little Before The Big 08:13
34 Tested Image Preview... Sort Of :) 07:36
35 Finishing Up With a Tested Wallpaper Update 10:44
36 Test Driven Wallpaper Delete - Part 1 11:11
37 Test Driven Wallpaper Delete - Part 2 11:57
38 EasyAdminBundle Login Form Tutorial 08:01