Introduction to Symfony Security
Welcome to Symfony Workshop. I'm Gary Clark and this is the first in a small series in which I'm going to cover Symfony security and authentication. I decided to do this series because the security bundle is powerful and the documentation is thorough, but there are a lot of options, a ridiculous amount of options. So hopefully I'm going to save you some time and some head scratching by showing you how to do it visually.
In this first one, I'm going to cover the concept of a user, a user provider, something called an authenticator, firewalls, which is another name for the authentication system as a whole, and we'll start on a login form.
Some information first, I'm recording high resolution so don't watch on a blurry screen. Choose high definition if that works for you. Would you like to join a growing group of PHP developers and take your skills to a new level? If that sounds like you, all you need to do is subscribe and click the little notification icon. And welcome.
Project Setup and Dependencies
I shall start off by creating a new Symfony project and I'll do that with symfony new. I'm going to call this security-demo, so that will be the project name and the folder name.
I'll move into that folder now and I'm going to require my dependencies, my composer dependencies. So composer require, and I'm going to need the security bundle, I'll need annotations, and I'm also going to need Doctrine, so this will be Doctrine ORM pack. All those will install super fast because I'm using Composer 2.
I'm also going to need a dev dependency which will be the web profiler and that will help me for debugging inside the web browser.
Reviewing Dependencies and Configuring Git
So that's installing. Let's now go over and check out the composer.json file and I'll show you what I've installed. So here's Doctrine: Doctrine migrations bundle and Doctrine ORM. Symfony security bundle. And here's my dev dependencies which is a web profiler bundle. That has its own dependencies: stopwatch and twig bundle.
Next, I'm going to use gitignore.io to generate a gitignore file for me. So if you've not used this before, it's very good. You put in here what you're using in your project. So for example, I'm using Symfony, I'm using PHPStorm, and it'll generate an ignore file which is relevant to those technologies. And I can just then copy this, go and paste it into my gitignore file, and it'll give me a nice starting point git ignore file. Because the Symfony stuff's okay but PHPStorm, it has this tendency to generate a load of files which once are in your project it's very hard to get them out. It requires a lot of work. So if you tackle that up front, then it can save you quite a lot of aggravation later on.
Initial Commit and Creating the User Entity
Now I'll make my first initial git commit because those bundles will have added stuff to the project. If you see that, I've got the web profiler yaml and annotation channel. And what I will say is that I'll separate my repo into different branches, and each branch will represent a recording, so this recording I will call branch one. Either branch one or recording one, you get the gist.
So there's one thing which I've got to install and that is the maker, which I'm going to need to create a user. So composer require maker. And now what I'm going to do is create a user. This class will be what represents an authenticated user. I do like a symphony console make:user. Name of the class: User. Do you want to store user data in the database via Doctrine? Yes. By doing it this way, it will generate a Doctrine entity for me called User. This is the most common way of doing it and I'm going to stick to the most common conventions. It also means I don't have to create a separate user provider class which I would have to do if I wasn't using a Doctrine entity for the user. This is referred to as an entity user provider. Next, I need to choose the property which is going to be used to identify the user, so I'll stick with email which is the default. And finally asks, does this application need to hash and check passwords? And the answer to that is yes.
Exploring the User Class and Security Configuration
Let's go and check out the user entity. So much like any other entity, it said to implement UserInterface. This is that interface and it represents the interface that all classes must implement. So there's a handful of methods in this interface which all user entities must include. So we'll go and have a look at the user entity and I'll show you where those are in there. Some you will use, some you might use, some you won't use I think in our case. So getUsername, getRoles, getPassword. Yep, getSalt, we won't be using that because we'll be using Sodium. And eraseCredentials maybe later on, I'll see if we might use that something.
So let's have a look at the security.yaml file. This is where we'll configure all of our security and authentication. As you can see, we have three keys: encoders, providers, and firewalls. I'll get to firewalls later, but for now, encoders. These are your password encoders. So use the user class name for the name and it says that it's going to auto-select the best possible hashing algorithm, that's what algorithm auto does. So it will choose Sodium when available. If that means nothing to you and you want to check it out, just Google libsodium.
Providers. So we said our app_user_provider will be of type entity and that we're going to use the App\Entity\User class and that the property that we're going to use to identify the user with will be the email property. Anyway, I think we're happy with our user entity so we can go ahead and create a migration for that.
Migrating the User Schema to the Database
And I do that with symfony console make:migration. That's created, so we'll go over to the project and we'll check out that migration. I'll tidy it up a little bit, that should make it a bit easier for you to read. So fairly straightforward. The only thing which might stand out is this: the roles is a json type field. The role field will be an array of roles which the user might have. As you can see, I've already gone ahead and created a database up front, I've called it security_app. And now all I need to do is run that migration and create the schema. So I do that with symfony console, then it's doctrine:migrations:migrate. And then it'll ask you if you're sure. The answer is yes. Let's go to database, give it a refresh, have a look at our table. So I've got a migrations table and we have our user table.
Manually Seeding the First User
What I'm going to need to do now is just insert a user because we don't have a registration form yet. We'll create one of those later on in another lesson, but for now we just need to get a user into the system so that we can practice logging in.
ID and email I'm sure you'll find fairly straightforward. Roles. So this needs to be a json array, and as you can see here in the getRoles method in the User class, it says guarantee every user at least has ROLE_USER. So one of the little quirks of Symfony security is that we have to guarantee that a user has at least this one role. This is the syntax for adding it to a json array.
Then I need to take care of the password. So the password also needs to be hashed. How am I going to do that, you wonder? Well the good news is that Symfony has thought of everything for us and there's a little tool where we can actually encode a password using the console. Just copy my command, and then you type in a password that you want to be encoded. It does the hard work for you and then where it says "Encoded password" I just need to go and grab all of that. It starts with argon. Over to my DB admin tool, I'm using TablePlus which I really like, paste that and hit command s and there we go, it's inserted that into my database. I have my one record.
Scaffolding the Authentication System with make:auth
Going back to the security yaml file, we have this firewalls key. This is probably the most important part, and this is your authentication system. This is where you tell Symfony how you would like to authenticate. And the way we're going to do it is we're going to do it using a login form and we're going to create a special authenticator class. There's a quick way of creating everything we need here and that is with symfony console make:auth.
If I select one, it will create a login form authenticator for us. I'll call that LoginFormAuthenticator, so I can call it anything I want. I'm going to call the controller which handles this AuthController rather than the default SecurityController, and I will also generate a logout route. That creates three brand new files and it updates the security yaml file. So I'm not totally keen on generating so many or auto-generating so many new files because I prefer to have more control of things myself. But what I'm going to do is the login form authenticator, I'm going to keep some of it but mainly customize it all myself.
That extends this class, which is the AbstractFormLoginAuthenticator. I intend to keep the properties and I'm going to keep the constructor, but the rest of it, all the methods, I'm going to delete and I'm going to redo from scratch myself. And I think that way is the best way to give you a good understanding of how this stuff works. As you can see, I've now got a red angry squiggly line because of certain methods which I'm meant to implement—five from the AuthenticatorInterface and one from the AbstractFormLoginAuthenticator. So that's the class that we're extending. Let's follow the path and we can see how we've reached all these things. So AbstractFormLoginAuthenticator extends AbstractGuardAuthenticator which implements AuthenticatorInterface, and that's what we're looking at now. And we'll need to add all of these methods to our class if they're not already added to one of the other classes which we are extending.
This authenticator class will be called into action before the request actually hits the controller, and the methods will be called in a sequence, a specified sequence.
Reviewing the Generated Authentication Components
Let's go and have a look at security.yaml and see what changes have happened there before I show you the login form. So here you can see there's a logout path which has been specified. And also if you look, we have a key called guard which takes an array of authenticators, and we just have the one LoginFormAuthenticator.
Our other two files are the AuthController and the login form. So this is the AuthController. We've created two routes and therefore we have two methods. One is login with the name of app_login. I think we'll just concern ourselves with login at the moment, and we'll deal with logout after. We also have a login.html.twig file which is basically just a plain login form. I'll take you and show you that now. Has two fields: email and password, and then a submit button, and also a hidden CSRF token field. We don't need to worry about the value of that because as you can see, Symfony is handling that for us.
Let's now start up the server and we'll go over and have a quick look at the login form. We won't actually be able to submit it yet because we've not put any of the authenticator logic in place, but at least we'll be able to have a look and see what we're dealing with. So I'll start Symfony's built-in server. I'll grab this URL. I'll append on login to that because that is what the route is. As you can see, a fairly plain looking login form.
This little bit in the profiler here where you have a little icon of a person gives you information regarding authentication. This tells us that we're authenticated anonymously using an anonymous token and that we're using the main firewall. You might be wondering, what does that mean? Let's go back to the security.yaml file and I'll show you what I mean with this anonymous login. So as you can see there, anonymous: true. It means that you can visit certain routes without actually needing to have logged in. You're visiting the login form anonymously because obviously you won't be an authenticated user if you still need to log in.
Conclusion and Next Steps
I'm going to take a pause there. Hopefully you're following along and you're learning stuff. Still plenty more to come. We need to style this form. We'll probably add things like remember me. We're going to do registration forms. And obviously we're going to fill out our authenticator and I'll show you the path from start to finish how it authenticates a user.
If you have enjoyed this one, be sure to give it a like and don't hesitate to share if you want to help out other developers like yourselves. That's what good developers do. And if you're on YouTube, to show you more of my content, all you need to do is subscribe and click the little notification icon. I release new material two times a week and details on my schedule can be found on the discussion tab on my YouTube channel homepage.