If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeTo use Guard - no matter what crazy authentication system you have - the first step is always to create an authenticator class. Create a new directory called Security
and inside, a new class: how about LoginFormAuthenticator
:
... lines 1 - 2 | |
namespace AppBundle\Security; | |
... lines 4 - 7 | |
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator; | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
... lines 12 - 30 | |
} |
The only rule about an authenticator is that it needs to extend AbstractGuardAuthenticator
. Well, not totally true - if you're building some sort of login form, you can extend a different class instead: AbstractFormLoginAuthenticator
- it extends that other class, but fills in some details for us.
Hit Command
+N
- or go to the "Code"->"Generate" menu - choose "Implement Methods" and select the first three:
... lines 1 - 4 | |
use Symfony\Component\HttpFoundation\Request; | |
use Symfony\Component\Security\Core\User\UserInterface; | |
use Symfony\Component\Security\Core\User\UserProviderInterface; | |
... lines 8 - 9 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
public function getCredentials(Request $request) | |
{ | |
} | |
public function getUser($credentials, UserProviderInterface $userProvider) | |
{ | |
} | |
public function checkCredentials($credentials, UserInterface $user) | |
{ | |
} | |
... lines 23 - 30 | |
} |
Then, do it again, and choose the other two:
... lines 1 - 9 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
... lines 12 - 23 | |
protected function getLoginUrl() | |
{ | |
} | |
protected function getDefaultSuccessRedirectUrl() | |
{ | |
} | |
} |
Tip
Starting in Symfony 3.1, you won't see getDefaultSuccessRedirectUrl()
in this list anymore.
Don't worry! We'll tell you how to handle this later.
That was just my way to get these methods in the order I want, but it doesn't matter.
When we're finished, Symfony will call our authenticator on every single request. Our job is to:
That all starts in getCredentials()
. Since this method is called on every request, we first need to see if the request is a login form submit. We setup our form so that it POSTs right back to /login
. So if the URL is /login
and the HTTP method is POST
, our authenticator should spring into action. Otherwise, it should do nothing: this is just a normal page.
Create a new variable called $isLoginSubmit
Set that to $request->getPathInfo()
- that's the URL - == '/login' && $request->isMethod('POST')
:
... lines 1 - 11 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
... lines 14 - 20 | |
public function getCredentials(Request $request) | |
{ | |
$isLoginSubmit = $request->getPathInfo() == '/login' && $request->isMethod('POST'); | |
... lines 24 - 34 | |
} | |
... lines 36 - 51 | |
} |
Tip
Instead of hardcoding the /login
URL, you could instead check for the current page's route name:
if ($request->attributes->get('_route') === 'security_login' && $request->isMethod('POST'))
If both of those are true, the user has just submitted the login form.
So, if (!$isLoginSubmit)
, just return null
:
... lines 1 - 22 | |
$isLoginSubmit = $request->getPathInfo() == '/login' && $request->isMethod('POST'); | |
if (!$isLoginSubmit) { | |
// skip authentication | |
return; | |
} | |
... lines 28 - 53 |
If you return null
from getCredentials()
, Symfony skips trying to authenticate the user and the request continues on like normal.
If the user is trying to login, our new task is to fetch the username & password and return them.
Since we built a form, let's let the form do the work for us.
Normally in a controller, we call $this->createForm()
to build the form:
... lines 1 - 38 | |
abstract class Controller implements ContainerAwareInterface | |
{ | |
... lines 41 - 274 | |
/** | |
* Creates and returns a Form instance from the type of the form. | |
* | |
* @param string|FormTypeInterface $type The built type of the form | |
* @param mixed $data The initial data for the form | |
* @param array $options Options for the form | |
* | |
* @return Form | |
*/ | |
protected function createForm($type, $data = null, array $options = array()) | |
{ | |
return $this->container->get('form.factory')->create($type, $data, $options); | |
} | |
... lines 288 - 396 | |
} |
In reality, this grabs the form.factory
service and calls create()
on it.
So how can we create a form in the authenticator? Use dependency injection to inject the form.factory
service.
Add a __construct()
method with a $formFactory
argument:
... lines 1 - 11 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
... lines 14 - 15 | |
public function __construct(FormFactoryInterface $formFactory) | |
{ | |
... line 18 | |
} | |
... lines 20 - 51 | |
} |
Now, I like to type-hint my arguments, so let's just guess at the service's class name and see if there's one called FormFactory
. Yep, there's even a FormFactoryInterface
!
... lines 1 - 5 | |
use Symfony\Component\Form\FormFactoryInterface; | |
... lines 7 - 11 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
... lines 14 - 15 | |
public function __construct(FormFactoryInterface $formFactory) | |
{ | |
... line 18 | |
} | |
... lines 20 - 51 | |
} |
That's probably what we want. I'll press Option
+Enter
and select "Initialize Fields" to set that property for me:
... lines 1 - 11 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
private $formFactory; | |
public function __construct(FormFactoryInterface $formFactory) | |
{ | |
$this->formFactory = $formFactory; | |
} | |
... lines 20 - 51 | |
} |
If you're still getting used to dependency injection and that all happened too fast, don't worry. We know we want to inject the form.factory
service, so I guessed its class for the type-hint, which is optional. You can always find your terminal and run:
./bin/console debug:container form.factory
to find out the exact class to use for the type-hint. We will also register this as a service in services.yml
in a minute.
Back in getCredentials()
, add $form = $this->formFactory->create()
and pass it LoginForm::class
:
... lines 1 - 4 | |
use AppBundle\Form\LoginForm; | |
... lines 6 - 11 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
... lines 14 - 20 | |
public function getCredentials(Request $request) | |
{ | |
$isLoginSubmit = $request->getPathInfo() == '/login' && $request->isMethod('POST'); | |
if (!$isLoginSubmit) { | |
// skip authentication | |
return; | |
} | |
$form = $this->formFactory->create(LoginForm::class); | |
... lines 30 - 34 | |
} | |
... lines 36 - 51 | |
} |
Then - just like always - use $form->handleRequest($request)
:
... lines 1 - 22 | |
$isLoginSubmit = $request->getPathInfo() == '/login' && $request->isMethod('POST'); | |
if (!$isLoginSubmit) { | |
// skip authentication | |
return; | |
} | |
$form = $this->formFactory->create(LoginForm::class); | |
$form->handleRequest($request); | |
... lines 31 - 53 |
Normally, we would check if $form->isValid()
, but we'll do any password checking or other validation manually in a moment. Instead, just skip to $data = $form->getData()
and return $data
:
... lines 1 - 22 | |
$isLoginSubmit = $request->getPathInfo() == '/login' && $request->isMethod('POST'); | |
if (!$isLoginSubmit) { | |
// skip authentication | |
return; | |
} | |
$form = $this->formFactory->create(LoginForm::class); | |
$form->handleRequest($request); | |
$data = $form->getData(); | |
return $data; | |
... lines 35 - 53 |
Since our form is not bound to a class, this returns an associative array with _username
and _password
. And that's it for getCredentials()
. If you return any non-null value, authentication continues to the next step.
Hey Terry,
I think you're right. This place can be error prone in future if the login URL changes. In the next chapter we inject a router into the authenticator, so you can easily reuse it to generate the URL.
Cheers!
I want to use different rules for authentication depending on which subdomain the user is logging in from.
What's the best way to get the subdomain inside of the LoginFormAuthenticator?
Hey Geoff,
As you probably know, $_SERVER does not contain any information about subdomains, but you can get the entire host from it. So, from Symfony's Request object, you can get the host like: $request->server->get('HTTP_HOST'). And if you know the main domain - then with a PHP string function you can easily get the subdomain.
Cheers!
Hello!
First things first:
In script it is: FormLoginAuthenticator and in code's script: LoginFormAuthenticator
The second problem is with Code->generate:
I don't have getDefaultSuccessRedirectUrl(); It's weird. Is it some kind of bug?
Ah yes, I think you're right about 3.1 Victor.
So, in 3.1, you can still override getDefaultSuccessRedirectUrl(), but you'll need to add the method to your authenticator yourself (if you go to Code -> Generate in PHPstorm, it won't be added for you anymore).
But more long-term, we removed this method (actually, I did it - it's my fault! https://github.com/symfony/symfony/pull/18135), and instead we want you to add a different method - called onAuthenticationSuccess(). If you're in Symfony 3.1, you should be able to do this:
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
use Symfony\Component\HttpFoundation\RedirectResponse;
class LoginFormAuthentication extends AbstractFormLoginAuthenticator
{
use TargetPathTrait;
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
// if the user hits a secure page and start() was called, this was
// the URL they were on, and probably where you want to redirect to
$targetPath = $this->getTargetPath($request->getSession(), $providerKey);
if (!$targetPath) {
$targetPath = $this->router->generate('homepage');
}
return new RedirectResponse($targetPath);
}
}
Basically, onAuthenticationSuccess() is really the method in charge of things. Previously (and it still works, but is deprecated in 3.1), you could just implement getDefaultSuccessRedirectUrl(), and this method would be taken care of for you. Feel free to do either (use getDefaultSuccessRedirectUrl() or the above code) - both will work, but the above code will be the way forward when Symfony 4 is released in a few years.
Cheers!
Thank you guys for reply!
I'm using symfony 3.1 but for this tutorial i will use the getDefaultSuccessRedirectUrl() method.
As you said it is still few years to symfony 4 ;)
Cheers!
Cheers back! Thanks for bringing this to our attention - I'm sure this conversation will help others with the same question :)
Yo Tony C!
It's not an accident :). Thanks to Symfony's backwards-compatibility promise, the way that's shown in the screencast still works, and will continue to work until Symfony 4.0. I'm actually the one who made this change to Symfony, so I'm well aware of it! We update the screencasts once a new major version of Symfony comes out - it's our way of balancing keeping things up to date, but not constantly re-recording things for tiny changes. It's not a perfect system :). Symfony's backwards compatibility promise helps us (and developers) out a lot with this!
But, in hindsight, I do think a note is in order - if you code with the tutorial perfectly, there's no problems. But if you use the PHPStorm shortcuts for "implement methods", then you won't see all the methods that we get in the recording - and that can be confusing indeed!
Cheers!
Well that's good to know! Thank you!
Backwards compatibility... a blessing and a curse all rolled up in one idea.
Haha, you nailed it - every time Symfony makes a change, I rejoice... and I cringe :D. I just added an issue internally to add a note to this chapter and the next (next chapter is where we actually code up this method).
Cheers!
Hey cybernet2u!
Haha, well, which part exactly? If you're using the LoginFormAuthenticator, the change is here: https://knpuniversity.com/screencast/symfony-security/login-form-authenticator#comment-2760429219. You'll also need to add a new supports() method, which does part of the job of getCredentials() in 4. So, like this:
public function supports(Request $request)
{
// if this returns true, then getCredentials() is called
return $request->getPathInfo() == '/login' && $request->isMethod('POST');
}
public function getCredentials(Request $request)
{
// no need to check that this is the login page anymore - it's done above
$form = $this->formFactory->create(LoginForm::class);
$form->handleRequest($request);
$data = $form->getData();
return $data;
}
Cheers!
For supports()
function, it might be useful to use this line:
public function supports(Request $request)
{
// if this returns true, then getCredentials() is called
return $request->getPathInfo() == $this->router->generate('security_login') && $request->isMethod('POST');
}
It might save us breaking something if we want to change /login
path, and <b>especially</b> if we are dealing with multi-lingual applications :)
Hey Yahya E.
It's not common to change the login's path name, but I totally agree with you, that code is more bullet proof
Cheers!
Hi Ryan, I have implemented this and it works beautifully however I have the impression that "getCredentials" no longer is called on every request... Could this be the case?
I'm want to update a custom field "last_access" on the user entity with a datetimestamp so I can keep track of who was online the last 5 minutes. I figured "getCredentials" would be the perfect place for that but unfortunately it does not get called (only at login).
Hey Dirk!
Ah yes, you're right! Once you update to the new Symfony 4 way (i.e. with the supports()) method, then supports()
IS called on every request. But, now, getCredentials()
is only called when supports() returns true. Actually, this is the whole purpose of supports(): Symfony is asking "Does your authenticator support trying to authenticate this request". If false is returned, getCredentials() is never called.
So, technically speaking, supports() would be the better place to move your last_access logic. But.... putting this logic here is not really the best spot. Simply because your authenticator is all about authentication, and updating this "last_access" is a totally different thing. Instead, I would create an "event susbcriber" that listens on the kernel.request event. This will have a similar effect: the method in your subscriber will be called on every request, and you can do whatever you want. By injecting the Security class (https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Security/Core/Security.php) into your event subscriber, you can easily get the User object so that you can update their field.
P.S. If you're using the MakerBundle on a Symfony 4 project, you can even generate a subscriber with:
php bin/console make:subscriber
Cheers!
Thanks Ryan! I did create an event subscriber and it works great! However, I was wondering how I can make sure that this is the last subscriber to be launched. The reason is that I'm updating the database with flush() and I do not want to launch other doctrine transactions prematurely... Is lowering the priority of the event an option? If yes, to what level?
Hey Dirk,
Yes, I think priority is an option for you. The priorities of the internal Symfony listeners usually range from -255 to 255 but your own listeners can use any positive or negative integer, so I suppose you can use a number which is lower than -255 to be sure your subscriber will be called *after* any Symfony one. And of course, make sure all your other listeners do not have a lower priority than the priority for that last subscriber. But it also sounds like a good spot to add some integration tests, at least to make sure everything works and you won't get any exceptions.
Cheers!
that tutorial i already followed, and even if the form was submitted, symfony never saw me as authenticated :( ( i did use the supports method )... now it's fixed, with other tutorial
weaverryan In the original implementation that is now deprecated you had the following lines that you no longer have... should we still have them, or do like the above and leave them out?
$targetPath = null;
// This comment wasn't in there, but the contents of this if is your $targetPath = line.
if ($request->getSession() instanceof SessionInterface) {}
Yo JSThePatriot!
Ah, interesting! That was a line that I didn't originally add to AbstractFormLoginAuthenticator
, but I see it! In the framework (unless you're doing something crazy), you shouldn't need this. In theory, you could configure things to not have a Session object at all - so getSession()
would return null (I'm not sure why they did it as an instanceof check, but they're basically checking to see if the Session exists). Feel free to keep it in, but you won't need it.
Good question man! Cheers!
Hey Ryan,
It all works until... onAuthenticationSuccess has to be called. If it isn't there, explosion. If it is there, no authentication required and I get taken to the page I requested, behind the firewall...
Hey odds!
Hmm. So if you're on Symfony 3.1, then you should have onAuthenticationSuccess
, but having it is technically optional until Symfony 4 (if you don't have it, you'll just receive a warning). But in either case, that method should have nothing to do with whether or not authentication is required for some endpoint - that's very strange. Let's debug!
A) If you don't have onAuthenticationSuccess, what is the error?
B) When you are taken to the page where no authentication is required, what do you see in the web debug toolbar for security? Does it say "anon"? And what code should be forcing authentication on that page? Do you have an access_control
or a denyAccessUnlessGranted
in a controller?
Let me know - we'll figure it out!
Cheers!
Hey Ryan,
I was a little bit to early with my conclusion. It works, but the toolbar does not show my username. That's why I thought something went horribly wrong...
Now trying to figure out why the toolbar shows n/a instead of my username...
Hey odds!
Ah, fascinating! The toolbar shows n/a? Hmm, what does your firewall look like? Usually, that toolbar only either shows (A) your username or (B) anon, if you're not logged in. n/a almost makes me think that the URL you're visiting isn't covered by your firewall. But that's just a first guess!
Cheers!
Hey Ryan!
I coded everything by the new updated way. So I'm on the login page and after I hit submit, it does nothing and always redirects back to the login page. It doesn't matter if using the correct password or a wrong one.
In the profiler the POST values are there, but in the form section it shows null.
UPDATE: After some extensive google search I got it working.
public function getCredentials(Request $request)
{
$isLoginSubmit = $request->getPathInfo() == '/login' && $request->isMethod('POST');
if ($isLoginSubmit) {
$req = $request->request->get('login_form');
return [
'_username' => $req['_username'],
'_password' => $req['_password'],
];
}
$form = $this->formFactory->create(LoginForm::class);
$form->handleRequest($request);
$data = $form->getData();
dump($data);
return $data;
}
Hey Attila László!
Ah, awesome! Usually, when you submit and see nothing it's either because your authenticator is not being called (i.e. it's not configured correctly in security.yml) or you have a bug in your getCredentials() method, so that it is not returning the credentials when it should.
In this case, it looks like you basically added some code to skip the form framework and instead grab the POST information directly from the request. That's a great way to do it, and actually, I wish I had done it this way - it's a bit simpler with no downside. You should be able to simplify your code:
public function getCredentials(Request $request)
{
$isLoginSubmit = $request->getPathInfo() == '/login' && $request->isMethod('POST');
if ($isLoginSubmit) {
$req = $request->request->get('login_form');
return [
'_username' => $req['_username'],
'_password' => $req['_password'],
];
}
// just return null: do no processing on this request
return;
}
I'm not sure what the problem was in your case... the form should have been processing the data just fine. But, I like your approach better anyways :).
Cheers!
Thx for the reply!
These are the files in question:
- https://pastebin.com/BTaQAEtX
- https://pastebin.com/CG1ET7CH
- https://pastebin.com/FWxzcz1m
- https://pastebin.com/TKfFzQhW
Hope we can figure it out, because I really want know these things.
Hey Attila László!
It's still a mystery to me! You're rendering your form correctly and handling the request in the authenticator just fine. From everything I can see, it *should* be properly processing the data through the form! So, I'm sorry I can't give you an answer! As I mentioned earlier, the way that does *not* use the form in the getCredentials() method is a little simpler/nicer anyways in my opinion (and I'll use that way personally on the future). So, stick with that :).
Cheers!
And is this a public or protected function? Since getDefaultSuccessRedirectUrl was protected...
Well, that's one question answered. This method should definitly be public (Symfony says so ;-) ), but than the shit hits the fan. The method getTargetPath is private (big explosion) and getDefaultSuccessRedirectUrl is not found... So, the stuff from the video is out of date and the stuff in the comment is incomplete. Bummer...
Hey Hermen!
The getTargetPath
function comes from the TargetPathTrait
. Make sure you're "using" this in your class. And you're right that getTargetPath
is private! But that's ok - it's legal to use a private method from a trait (effectively, when you "use" a trait, its methods are copied into your class - so you CAN access private methods).
Let me know if that helps!
I just looked at the TargetPathTrait
and it only has 3 functions, Save, Get and Remove. The getDefaultSucessRedirectUrl
has been removed - v3.2.3.
Am I looking at the wrong spot? or should I just do the redirect myself (Figured what the hell, just create it until I get a response :P )?
Peter Ah, you're right! I mis-spoke in my previous message! Basically, the solution in the video works, but we made some changes in Symfony, so the new "non-deprecated" version is available above in my comment: https://knpuniversity.com/screencast/symfony-security/login-form-authenticator#comment-2760429219
Basically, you should now implement onAuthenticationSuccess
yourself, and inside, you'll make use of getTargetPath()
. If you do this, then you don't need the getDefaultSuccessRedirectUrl
at all :). But, I can see now that my code-block above is mis-leading on this! So, Peter, you did the right thing by adding getDefaultScucesRedirectUrl
yourself, but it's actually not needed. Check (in about 2 minutes) my updated code above the comment I linked to :).
Cheers!
And actually, I had some misleading code in my comment above! You should not need to call getDefaultSuccessRedirectUrl
at all anymore in the new way - my misleading code is what confused you on this! I've just updated the original code in my comment above with the not-misleading, proper way.
Cheers!
Hey there
is this due to the version of symfony? It may... How are you injecting the "formFactory" argument?
Thank you Diego, Yeah absolutely I have a problem with injection 'autowire'.
thank you very much.
In that case you have to explicitly enable it by adding autowire: true
in your service definition.
I must be missing something. I've been following the course with 3.3.12 and up until the autowiring, which I skipped (since 3.3.12 does that for me). Now I'm getting the error "Class Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator not found" for some reason. If I add 'Security' to my exclude pattern, the service cannot be found ("exclude: '../../src/AppBundle/{Entity,Repository,Tests,Security}'"). I'm currently at a loss as how this is occurring...
Alright. I am that guy that honestly posts after 1,5 hour of searching and after hitting enter finds out that he corrupted the AbstractFormLoginAuthenticator class @trigger_error portion by reading up and removing the semicolon.
Eventually I also found out that declaring _defaults in the services.yml doesn't work globally. I'm also using AppBundle specific Resources which also include a set of services (in the .yml file). Upon copying those defaults introduced in 3.3 it all worked like a charm.
// composer.json
{
"require": {
"php": ">=5.5.9",
"symfony/symfony": "3.1.*", // v3.1.4
"doctrine/orm": "^2.5", // v2.7.2
"doctrine/doctrine-bundle": "^1.6", // 1.6.4
"doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
"symfony/swiftmailer-bundle": "^2.3", // v2.3.11
"symfony/monolog-bundle": "^2.8", // 2.11.1
"symfony/polyfill-apcu": "^1.0", // v1.2.0
"sensio/distribution-bundle": "^5.0", // v5.0.22
"sensio/framework-extra-bundle": "^3.0.2", // v3.0.16
"incenteev/composer-parameter-handler": "^2.0", // v2.1.2
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"knplabs/knp-markdown-bundle": "^1.4", // 1.4.2
"doctrine/doctrine-migrations-bundle": "^1.1" // 1.1.1
},
"require-dev": {
"sensio/generator-bundle": "^3.0", // v3.0.7
"symfony/phpunit-bridge": "^3.0", // v3.1.3
"nelmio/alice": "^2.1", // 2.1.4
"doctrine/doctrine-fixtures-bundle": "^2.3" // 2.3.0
}
}
At about the 2 minute mark, you hard code "/login". It would be better to somehow call the path "security_login" created in the routing of the controller, correct? In case "/login" later changes to something else.