EasyAdminBundle Login Form Tutorial
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 :)