If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
So where’s the actual login form? Well, that’s our job - the security layer just helps us by redirecting the user here.
Oh, and there’s a really popular open source bundle called FosUserBundle that gives you a lot of what we’re about to build. The good news is that after building a login system in this tutorial, you’ll better understand how it works. So build it once here, then take a serious look at FosUserBundle.
Let’s create a brand new shiny bundle called UserBundle for all of our user and security stuff. We could use the app/console generate:bundle task to create this, but let’s do it by hand. Seriously, it’s easy.
Just create a UserBundle directory and an empty UserBundle class inside of it. A bundle is nothing more than a directory with a bundle class:
// src/Yoda/UserBundle/UserBundle.php
namespace Yoda\UserBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class UserBundle extends Bundle
{
}
Now, just activate it in the AppKernel class and, voila! Our brand new shiny bundle is ready:
// app/AppKernel.php
// ...
public function registerBundles()
{
$bundles = array(
// ...
new Yoda\UserBundle\UserBundle(),
);
// ...
}
To make the login page, add a Controller directory and put a new SecurityController class inside of it. Give the class a loginAction method. This will render our login form:
// src/Yoda/UserBundle/Controller/SecurityController.php
namespace Yoda\UserBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class SecurityController extends Controller
{
public function loginAction()
{
}
}
Before we fill in the guts of loginAction, we need a route! After watching episode 1, you probably expect me to create a routing.yml file in UserBundle and add a route there.
Ha! I’m not so predictable! Instead, we’re going to get crazy and build our routes right inside the controller class using annotations. The docs for this feature live at symfony.com under a bundle called SensioFrameworkExtraBundle. This bundle came pre-installed in our project. How thoughtful!
First, add the Route annotation namespace:
// src/Yoda/UserBundle/Controller/SecurityController.php
// ...
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
class SecurityController extends Controller
{
// ...
}
Now, we can add the route right above the method:
// src/Yoda/UserBundle/Controller/SecurityController.php
// ...
/**
* @Route("/login", name="login_form")
*/
public function loginAction()
{
// ... todo still..
}
Finally, tell Symfony to look for routes in our controller by adding an import to the main routing.yml file:
# app/config/routing.yml
# ...
user_routes:
resource: "@UserBundle/Controller"
type: annotation
Remember that Symfony never automatically finds routing files: we always import them manually from here.
Cool - change the URL in your browser to /login. This big ugly error about our controller not returning a response is great news! No, seriously, it means that the route is working. Now let’s fill in the controller!
Most of the login page code is pretty boilerplate. So let’s use the age-old art of copy-and-paste from the docs.
Head to the security chapter and find the login form section. Copy the loginAction and paste it into our controller. Don’t forget to add the use statements for the SecurityContextInterface and Request classes:
// src/Yoda/UserBundle/Controller/SecurityController.php
namespace Yoda\UserBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Symfony\Component\HttpFoundation\Request;
// ...
class SecurityController extends Controller
{
/**
* @Route("/login", name="login")
*/
public function loginAction(Request $request)
{
$session = $request->getSession();
// get the login error if there is one
if ($request->attributes->has(SecurityContextInterface::AUTHENTICATION_ERROR)) {
$error = $request->attributes->get(
SecurityContextInterface::AUTHENTICATION_ERROR
);
} else {
$error = $session->get(SecurityContextInterface::AUTHENTICATION_ERROR);
$session->remove(SecurityContextInterface::AUTHENTICATION_ERROR);
}
return $this->render(
'AcmeSecurityBundle:Security:login.html.twig',
array(
// last username entered by the user
'last_username' => $session->get(SecurityContextInterface::LAST_USERNAME),
'error' => $error,
)
);
}
The method just renders a login template: it doesn’t handle the submit or check to see if the username and password are correct. Another layer handles that. It does pass the login error message to the template if there is one, but that’s it.
The pasted code is rendering a template using our favorite render method that lives in Symfony’s base controller.
Hmm, let’s not do this. Instead, let’s use another shortcut: the @Template annotation, which is also from SensioFrameworkExtraBundle.
Anytime we use an annotation in a class for the first time, we’ll need to add a use statement for it. Copy this from the docs. Now, put @Template above the method and just return the array of variables you want to pass to Twig:
// src/Yoda/UserBundle/Controller/SecurityController.php
// ...
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
class SecurityController extends Controller
{
/**
* @Route("/login", name="login_form")
* @Template()
*/
public function loginAction()
{
// ...
return array(
// last username entered by the user
'last_username' => $session->get(SecurityContextInterface::LAST_USERNAME),
'error' => $error,
);
}
}
With @Template, Symfony renders a template automatically, and passes the variables we’re returning into it. It’s cool, saves us some typing and supports the rebel forces.
Hey Scott!
Yea, I think this issue :). Actually, when you login, you *are* logged in for a moment, but then you lose the login on the next request. You can see this by setting intercept_redirects (https://github.com/symfony/... to true temporarily. After login, it will stop and *not* redirect you. And on this page, you'll see that you *were* successfully authenticated.
So, why is this lost on the next request? It's a gotcha (one that hopefully we can make less of a gotcha in the future - it's tricky). Three things happen at the beginning of each request after you're logged in:
A) The User object is deserialized from the session (that's when the User::unserialize method is called)
B) That User object is used to query for a new one: refreshUser is called on your user provider. Usually, you use the id of the serialized User to query for a fresh one.
C) (and here's the tricky one): the deserialized User object is compared with the fresh User object. If they are not deemed "equal", you are logged out. What??? The purpose of this is to allow you to change your password in the database and cause a remote bad person who has stolen your account to be automatically logged out. Basically, you need to make sure that several properties are serialized: username, password, salt (not relevant in your case) and any values that fuel the AdvancedUserInterface methods. As long as these are serialized, then the two User objects will look equal. Here's the code in Symfony that checks this: https://github.com/symfony/....
The most common problem is not serializing some properties you need in User::serialize(). But you did a perfect job! The issue is smaller, and I've described it more on this pull request: https://github.com/onlinesp...
I hope that helps! Also, to make your code simpler, you could delete your UserProvider (and associated code in UserRepository) and instead user the built-in entity user provider (you have some commented-out code for this - it looks like you *were* using it. Anymore, I don't really think that *anyone* needs a custom user provider if they're using Doctrine. If you need to do some crazy query for your User in your "authenticator", just call a custom method on your repository. You're already doing this: https://github.com/weaverry...
Also, you can remove the "form_login" stuff if you're using guard as your login form - it's just not needed. You *will* still you need your login routes, template, etc.
Cheers!
I really appreciate your help with this and all the other stuff you do for the community!
I thought that was what the application was doing (logging in and logging back out) but didn't know about the intercept_redirects setting. Your explanation makes sense.
I merged the change in the pull request, deleted the UserRepository and references to it, and removed the "form_login" stuff and everything is working. Yay!
Now off to implement API and Facebook authentication!
Wow, that's awesome!!!
It's on my todo list to add some things for social (Facebook) auth, but I haven't gotten there quite yet. I *do*, however, have a WIP code implementation of Facebook auth using the KnpGuardBundle (which is very close to the core Guard). If it's helpful, you can see that final code here: https://github.com/knpunive..., with step-by-step commits: https://github.com/knpunive...
Good luck!
Thanks again!
I was able to implement Login, API, and Facebook authentication. Initial versions here: https://github.com/onlinesp...
Wouldn't have been able to finish this so quickly without your help!
Hi everyone. I asked this before in an old thread, but I think this is a better idea to put my question in here
I'm having this problem after following the steps and I'm a bit blocked. My symfony version is 3.2.8
Here is the output I got when I try to run my application
InvalidConfigurationException in ArrayNode.php line 317:
Unrecognized option "knpu_guard" under "security.firewalls.main"
Do I missed something ?
Thanks in advance!
I can ask myself: The article must be a bit deprecated because knpu_guard is no longer accepted. If you have this problem use "guard" instead as a key in the security.yml file
Hey Sergio Valije Guiadanes!
Yes, you're right! Guard was accepted into Symfony's core in version 2.8 - before, that, it was a third-party bundle and was configured via knpu_guard. There are a few other differences as well with the new version in core - so just be careful! It's on our list to properly update our Guard tutorial for the version in Symfony's core.
Cheers!
Followed this tutorial to the 'T' and still cannot login. No errors, just tells me 'Username could not be found.'
Oh no!
Let's debug this :). How far are you through the tutorial? I'm asking because in the beginning, we're still loading users from our "hardcoded" list in security.yml. Later, we load from the database. But in more recent versions of Symfony, we've *changed* what the out-of-box security.yml file looks like - and it *no* longer includes these "hardcoded" users by default. That *might* be the cause of the problem. Here's what the entire security.yml file originally looked like in this tutorial at the end of the next chapter (so, after we finish the login form stuff): https://gist.github.com/wea...
Or, it could be something entirely different - let me know what your setup looks like :). The good news is that this error tells us that the User couldn't be found - so we can rule out there being some problem with checking the user's password.
Cheers!
Hello guys!
Right now, the only way I saw is to create a SecurityController and inside it a loginAction() method which has it's own path (/login), and which, at the end, will render a security/login.html.twig template containing the login form. Also in security.yml there is an access_control with - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY } entry.
How can I change all this behavior? As I want to have the login and register forms in my homepage, and not each of them in their separate pages.
Tried to use, in my index.html.twig (which is my homepage template), something like:
{% block body %}
{# ... #}
{% render(controller('AppBundle:Security:login')) %}
{# ... #}
{% endblock %}
but this throws an error:
Unexpected "render" tag (expecting closing tag for the "block" tag defined near line X) in default/index.html.twig at line Y.
I also tried to change the template of the loginAction() method to 'default/index.html.twig', and write the login form in this template, as:
{% if error %}
<div>{{ error.messageKey|trans(error.messageData, 'security') }}</div>
{% endif %}
<form action="{{ path('login') }}" method="post">
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
<div class="form-group">
<label for="email">Your E-mail</label>
<input type="email" class="form-control" name="email" id="email">
</div>
<div class="form-group">
<label for="first_name">Your First Name</label>
<input type="text" class="form-control" name="first_name" id="first_name" value="{{ last_username }}">
</div>
<div class="form-group">
<label for="password">Your Password</label>
<input type="password" class="form-control" name="_password" id="password">
</div>
<button type="button" class="btn btn-primary">Submit</button>
</form>
The errors where:
1. Variable "error" does not exist in default/index.html.twig at line Z. (tried to fix this by adding "if error is defined" - the error was gone, but I think is not the right way to fix it)
2. Variable "last_username" does not exist in default/index.html.twig at line T. (and I don't know why this variable is not being send from loginAction() to 'default/index.html.twig' as is specified there)
I'll appreciate any suggestion!
Thanks!
Hey Dan,
You should use "print something - {{ }}" syntax instead of "do something {% %}", here's a right solution:
{{ render(controller('AppBundle:Security:login')) }}
This will fix "Unexpected render tag..." error.
Use is defined test or default Twig filter in template:
{% if error is defined %}
{# or #}
{% if error|default(false) %}
Btw, you could only render both forms on homepage, but process them on its separate pages, just use the correct action in your forms. If user leaves some validation errors - he'll end up on form login page/ registration page. But if user fills form correct - he will be processed and won't see login/registration pages at all.
Cheers!
Yes it is! But it's a simple deprecation :) - the constants were moved to a class called Security. So, the new way is:
use Symfony\Component\Security\Core\Security;
// ...
Security::AUTHENTICATION_ERROR
Btw - this is the tutorial for Symfony 2 - we have an updated Security tutorial for Symfony 3: http://knpuniversity.com/sc... - it has all the latest and greatest ways of doing things :).
Cheers!
To complete the authentication is necessary to return the value true on method checkCredentials
Hey, Daniel!
Are you talking about checkCredentials()
method which should be implemented from AbstractGuardAuthenticator
? If so, then yes, you should return "true" to cause authentication success.
<blockquote>If getCredentials() returns a non-null value, then this method is called and its return value is passed here as the $credentials argument. Your job is to return an object that implements UserInterface. If you do, then checkCredentials() will be called. If you return null (or throw an AuthenticationException) authentication will fail.
</blockquote>
You can find explanation of other methods on <a href="http://symfony.com/doc/current/cookbook/security/guard-authentication.html#the-guard-authenticator-methods">The Guard Authenticator Methods</a> page.
Cheers!
when i submit submit the form to the "login_check" path i get this error:
ClassNotFoundException in appDevDebugProjectContainer.php line 1654: Attempted to load class "FormLoginAuthenticator" from namespace "Arably\RestBundle\Security".
Did you forget a "use" statement for another namespace?
I use symfony2.8, FosUserBundle as the user provider and mongodb odm
and this is my services file:
services:
form_login_authenticator:
class: Arably\RestBundle\Security\FormLoginAuthenticator
autowire: true
FormLoginAuthenticator class:
namespace Arably\RestBundle\Security;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
class FormLoginAuthenticator extends AbstractGuardAuthenticator
{
The methods implemented
}
SecurityController:
namespace Arably\RestBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class SecurityController extends Controller
{
public function loginAction()
{
}
public function loginCheckAction()
{
// will never be executed
}
}
i am using a rest API so i don't have a login page provided by the server
Hi there!
If you're building an API, then there's limited useful ness to using FOSUserBundle (you can use it still for your User document, but you won't use any of its other features). In this case, your error is just because you have a bad namespace (or something) with your authenticator. The namespace and services.yml look good to me. So, does this file live in the correct location? It should be "src/Arably/RestBundle/Security/FormLoginAuthenticator.php".
Btw, we do have a tutorial about API authentication - it might be helpful! http://knpuniversity.com/sc...
Cheers!
Thanks for you response. I finally implemented the check login method and generated the JWT in it.
What do I use for the roles field in the user table? And how to I tell what role is the user logged in as after login?
Hey cristicitea ,
What do you mean in the first question? What type of the roles field should you use? or something different?
Actually, you should add a role for user before he will be logged in. The most common way is to set some role, like ROLE_USER when user is doing registration, i.e. when you're creating a new user in the DB. So when users are logging in - they _already_ have some role. Then you can easily check wether a logged in user has the required role with isGranted() method, i.e. $this->isGranted('ROLE_USER') in your controller (but only if you extends the Symfony\Bundle\FrameworkBundle\Controller\Controller class) or use @security.authorization_checker service otherwise.
Cheers!
so, do I literally add ROLE_USER in the roles
field in the database? Also, will the security file catch that role in the`
access_control`
automatically?
for example, if i am logged in as an admin, will this code understand that I am logged in as admin?
{ path: ^/test, roles: ROLE_ADMIN }```
Hey cristicitea ,
Yes, you should store user roles in database, for example when you store user information during registration. And yes, Symfony app should understand, but try it to double check - sometimes you could miss something in config. Take a look at our course Symfony Security: Beautiful Authentication, Powerful Authorization - it has nice example about dynamic user roles at the end.
Cheers!
ok, I figure it out. the roles field in the db takes an array`
["ROLE_USER"]`
. And using an annotation works as well`
@Security("is_granted('ROLE_USER')")`
Yes, that's great! Actually, I use both access_control list and @Security
annotation to be sure with proper security. Even more, you can double check if user has access with custom call of isGranted() method in actions. ;)
Cheers!
Color me a bit confused. The FormLoginAuthenticator::getCredentials() method includes this line:
$request->getSession()->set(Security::LAST_USERNAME, $username);
but I don't see a use for Anything\...\Security.
Where does it come from?
There's a use statement at the top of that file:
use Symfony\Component\Security\Core\Security;
I just forgot to "show" it in the code-block. I'll fix that now!
This is my app config routing:
user:
resource: "@UserBundle/Resources/config/routing.yml"
prefix: /
app:
resource: "@AppBundle/Controller/"
type: annotation
user_routes:
resource: "@UserBundle/Controller"
type: annotation
I'm still getting the error:
No route found for "GET /login"
Any idea. I read the other comments but I check twice and I have all my @
Hey dash!
You already guessed my first debugging technique - bin/console debug:router. If you see /login listed, then you should not be getting this "No route found for GET /login" error. I mean, really, it's basically impossible - so something *very* strange is happening! I would click the web debug toolbar to go into the profiler. Then, go to the Routing tab. This will show you all of the routes that were searched. Do you see /login there?
Cheers!
Is anybody else having difficulty getting the route /login to register with Symfony? I get a "No route found for "GET /login". Also the route isn't listed when using route:debug!
If it helps, you can view my Controller and routing file on my GitHub - https://github.com/sjhuda/k...
Any ideas? :/
Soooo, after a day of debugging...I looked at the other annotation routes in the Event Controller and I missed the @ infront of 'Route'. *facepalm*
Bah, bummer! But good debugging! And sorry for the late reply! I especially liked that you ran router:debug - that is always what I do first when something happens that I don't expect with routing.
Good luck! At least you won't make that mistake again :)
No worries on the delay. It actually gave me a better understanding of Routing (and especially annotations).
Keep up the good work, love all your videos.
Hi i have a problem! I i use SecurityContextInterface the code give me an error! but SecurityContextInterface is not deprecated??
Hey Marco!
What's the error? The SecurityContextInterface *is* deprecated, but it obviously still exists (unless you're using Symfony 3.0, then it's removed). Either way, use the Symfony\Component\Security\Core\Security class instead. We'll be releasing updated tutorials with all the new classes you should use soon :).
Cheers!
// composer.json
{
"require": {
"php": ">=5.3.3",
"symfony/symfony": "~2.4", // v2.4.2
"doctrine/orm": "~2.2,>=2.2.3", // v2.4.2
"doctrine/doctrine-bundle": "~1.2", // v1.2.0
"twig/extensions": "~1.0", // v1.0.1
"symfony/assetic-bundle": "~2.3", // v2.3.0
"symfony/swiftmailer-bundle": "~2.3", // v2.3.5
"symfony/monolog-bundle": "~2.4", // v2.5.0
"sensio/distribution-bundle": "~2.3", // v2.3.4
"sensio/framework-extra-bundle": "~3.0", // v3.0.0
"sensio/generator-bundle": "~2.3", // v2.3.4
"incenteev/composer-parameter-handler": "~2.0", // v2.1.0
"doctrine/doctrine-fixtures-bundle": "~2.2.0", // v2.2.0
"ircmaxell/password-compat": "~1.0.3", // 1.0.3
"phpunit/phpunit": "~4.1" // 4.1.0
}
}
I am having an issue getting form login to work in Symfony 3 using Guard for authentication and mysql database.
I have read everything on this site, symfony.com and your awesome slideshare @ http://www.slideshare.net/w.... But, I think I am missing something.
What I am trying to do:
- Allow user to log in
- Redirect user to home page upon login
I have created a login form. But, when I try to log in with the users I have created, I am redirected back to the login page and the user is only authenticated as Anonymous.
I also have a registration page. And, when I register a new user, I am able to authenticate the user in the registerAction function of the RegistrationController and the user is sent to the home page as expected and shows as authenticated as the user.
But, after logging the user out and trying to log in the newly created user, I am redirected back to the login page and the user is only authenticated as Anonymous.
Trying to debug, the checkCredentials function in the FormLoginAuthenticator seems to pass and is returning true and the onAuthenticationSuccess function is called.
I am relatively new to Symfony and am sure that I am missing something. But, after looking over it many times, I can't seem to figure out what is going on.
Anyway you could take a quick look at it?
https://github.com/onlinesp...
Thanks in advance,
Scott