Authenticator: getUser, checkCredentials & Success/Failure
Keep on Learning!
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 SubscribeHere's the deal: if you return null
from getCredentials()
, authentication is skipped. But if you return anything else, Symfony calls getUser()
:
// ... lines 1 - 8 | |
use Symfony\Component\Security\Core\User\UserProviderInterface; | |
// ... lines 10 - 11 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
// ... lines 14 - 36 | |
public function getUser($credentials, UserProviderInterface $userProvider) | |
{ | |
} | |
// ... lines 40 - 51 | |
} |
And see that $credentials
argument? That's equal to what we return in getCredentials()
. In other words, add $username = $credentials['_username']
:
// ... lines 1 - 12 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
// ... lines 15 - 39 | |
public function getUser($credentials, UserProviderInterface $userProvider) | |
{ | |
$username = $credentials['_username']; | |
// ... lines 43 - 45 | |
} | |
// ... lines 47 - 58 | |
} |
I do continue to call this username, but in our case, it's an email address. And for you, it could be anything - don't let that throw you off.
Hello getUser()
Our job in getUser()
is... surprise! To get the user! What I mean is - to somehow return a User
object. Since our Users are stored in the database, we'll query for them via the entity manager. To get that, add a second constructor argument: EntityManager $em
:
// ... lines 1 - 12 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
// ... lines 15 - 17 | |
public function __construct(FormFactoryInterface $formFactory, EntityManager $em) | |
{ | |
// ... lines 20 - 21 | |
} | |
// ... lines 23 - 58 | |
} |
And once again, I'll use my Option
+Enter
shortcut to create and set that property:
// ... lines 1 - 12 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
// ... line 15 | |
private $em; | |
public function __construct(FormFactoryInterface $formFactory, EntityManager $em) | |
{ | |
// ... line 20 | |
$this->em = $em; | |
} | |
// ... lines 23 - 58 | |
} |
Now, it's real simple: return $this->em->getRepository('AppBundle:User')->findOneBy()
with email => $email
:
// ... lines 1 - 12 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
// ... lines 15 - 39 | |
public function getUser($credentials, UserProviderInterface $userProvider) | |
{ | |
$username = $credentials['_username']; | |
return $this->em->getRepository('AppBundle:User') | |
->findOneBy(['email' => $username]); | |
} | |
// ... lines 47 - 58 | |
} |
Easy. If this returns null
, guard authentication will fail and the user will see an error. But if we do return a User
object, then on we march! Guard calls checkCredentials()
:
// ... lines 1 - 12 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
// ... lines 15 - 47 | |
public function checkCredentials($credentials, UserInterface $user) | |
{ | |
} | |
// ... lines 51 - 58 | |
} |
Enter checkCredentials()
This is our chance to verify the user's password if they have one or do any other last-second validation. Return true
if you're happy and the user should be logged in.
For us, add $password = $credentials['_password']
:
// ... lines 1 - 12 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
// ... lines 15 - 47 | |
public function checkCredentials($credentials, UserInterface $user) | |
{ | |
$password = $credentials['_password']; | |
// ... lines 51 - 56 | |
} | |
// ... lines 58 - 65 | |
} |
Our users don't have a password yet, but let's add something simple: pretend every user shares a global password. So, if ($password == 'iliketurtles')
, then return true
:
// ... lines 1 - 49 | |
$password = $credentials['_password']; | |
if ($password == 'iliketurtles') { | |
return true; | |
} | |
return false; | |
// ... lines 57 - 67 |
Otherwise, return false
: authentication will fail.
When Authentication Fails? getLoginUrl()
That's it! Authenticators are always these three methods.
But, what happens if authentication fails? Where should we send the user? And what about when the login is successful?
When authentication fails, we need to redirect the user back to the login form. That will happen automatically - we just need to fill in getLoginUrl()
so the system knows where that is:
// ... lines 1 - 12 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
// ... lines 15 - 58 | |
protected function getLoginUrl() | |
{ | |
} | |
// ... lines 62 - 65 | |
} |
But to do that, we'll need the router
service. Once again, go back to the top and add another constructor argument for the router. To be super cool, you can type-hint with the RouterInterface
:
// ... lines 1 - 8 | |
use Symfony\Component\Routing\RouterInterface; | |
// ... lines 10 - 13 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
// ... lines 16 - 19 | |
public function __construct(FormFactoryInterface $formFactory, EntityManager $em, RouterInterface $router) | |
{ | |
// ... lines 22 - 24 | |
} | |
// ... lines 26 - 70 | |
} |
Use the Option
+Enter
shortcut again to set up that property:
// ... lines 1 - 13 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
// ... lines 16 - 17 | |
private $router; | |
public function __construct(FormFactoryInterface $formFactory, EntityManager $em, RouterInterface $router) | |
{ | |
// ... lines 22 - 23 | |
$this->router = $router; | |
} | |
// ... lines 26 - 70 | |
} |
Down in getLoginUrl()
, return $this->router->generate('security_login')
:
// ... lines 1 - 13 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
// ... lines 16 - 61 | |
protected function getLoginUrl() | |
{ | |
return $this->router->generate('security_login'); | |
} | |
// ... lines 66 - 70 | |
} |
When Authentication is Successful?
Tip
Due to a change in Symfony 3.1, you can still fill in getDefaultSuccessRedirectUrl()
like we do here, but it's deprecated. Instead, you'll add a different
method - onAuthenticationSuccess()
- we have the code in a comment:
http://bit.ly/guard-success-change
So what happens when authentication is successful? It's awesome: the user is automatically redirected back to the last page they tried to visit before being forced to login. In other words, if the user tried to go to /checkout
and was redirected to /login
, then they'll automatically be sent back to /checkout
so they can continue buying your awesome stuff.
But, in case they go directly to /login
and there is no previous URL to send them to, we need a backup plan. That's the purpose of getDefaultSuccessRedirectUrl()
:
// ... lines 1 - 13 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
// ... lines 16 - 66 | |
protected function getDefaultSuccessRedirectUrl() | |
{ | |
// ... line 69 | |
} | |
} |
Send them to the homepage: return $this->router->generate('homepage')
:
// ... lines 1 - 13 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
// ... lines 16 - 66 | |
protected function getDefaultSuccessRedirectUrl() | |
{ | |
return $this->router->generate('homepage'); | |
} | |
} |
The authenticator is done. If you need even more control over what happens on error or success, there are a few other methods you can override. Or check out our Guard tutorial. Let's finally hook this thing up.
Registering the Service
To do that, open up app/config/services.yml
and register the authenticator as a service.
Tip
If you're using Symfony 3.3, your app/config/services.yml
contains some extra code
that may break things when following this tutorial! To keep things working - and learn
about what this code does - see https://knpuniversity.com/symfony-3.3-changes
Let's call it app.security.login_form_authenticator
. Set the class to LoginFormAuthenticator
and because I'm feeling super lazy, autowire the arguments:
// ... lines 1 - 5 | |
services: | |
// ... lines 7 - 17 | |
app.security.login_form_authenticator: | |
class: AppBundle\Security\LoginFormAuthenticator | |
autowire: true |
We can do that because we type-hinted all the constructor arguments.
Configuring in security.yml
Finally, copy the service name and open security.yml
. To activate the authenticator, add a new key under your firewall called guard
. Add authenticators
below that, new line, dash and paste the service name:
// ... lines 1 - 2 | |
security: | |
// ... lines 4 - 9 | |
firewalls: | |
// ... lines 11 - 15 | |
main: | |
// ... line 17 | |
guard: | |
authenticators: | |
- app.security.login_form_authenticator | |
# activate different ways to authenticate | |
// ... lines 22 - 28 |
As soon as we do that, getCredentials()
will be called on every request and our whole system should start singing.
Let's try it! Try logging in with weaverryan+1@gmail.com
, but with the wrong password.
Beautiful! Now try the right password: iliketurtles
.
Debugging with intercept_redirects
Ah! Woh! It did redirect to the homepage as if it worked, but with a nasty error. In fact, authentication did work, but there's a problem with fetching the User from the session. Let me prove it by showing you an awesome, hidden debugging tool.
Open up config_dev.yml
and set intercept_redirects
to true:
// ... lines 1 - 12 | |
web_profiler: | |
// ... line 14 | |
intercept_redirects: true | |
// ... lines 16 - 49 |
Now, whenever the app is about to redirect us, Symfony will stop instead, and show us the web debug toolbar for that request.
Go to /login
again and login in with weaverryan+1@gmail.com
and iliketurtles
. Check this out: we're still at /login
: the request finished, but it did not redirect us yet. And in the web debug toolbar, we are logged in as weaverryan+1@gmail.com
.
So authentication works, but there's some issue with storing our User in the session. Fortunately, that's going to be really easy to fix.
when using $this->getUser() getting this error
The Acme\ApiBundle\Security\JwtTokenAuthenticator::getUser() method must return a UserInterface. You returned Acme\ApiBundle\Entity\User.
when implement UserInterface in my User entity I get a null response from $this->getUser()
how to solve this?