If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
My favorite new feature for Symfony 2.8 is Guard. It makes creating custom and crazy authentication systems really really easy. I'm a bit biased: Guard was my creation, inspired by a lot of people and projects.
Later, I'll do some in-depth screencasts about Guard, but I want to give you a taste of what's possible. In this project, I have a tradition login system:
... lines 1 - 21 | |
/** | |
* @Route("/login", name="login") | |
*/ | |
public function sillyLoginAction() | |
{ | |
$error = $this->get('security.authentication_utils') | |
->getLastAuthenticationError(); | |
return $this->render('default/login.html.twig', [ | |
'error' => $error | |
]); | |
} | |
... lines 34 - 42 |
The /login
controller renders a login template and this template builds a form
that POSTs right back to that same loginAction
:
... lines 1 - 9 | |
<form action="{{ path('login') }}" method="POST"> | |
... line 11 | |
<label for="exampleInputEmail1">Username</label> | |
<input type="text" name="_username" class="form-control" id="exampleInputEmail1" placeholder="Username"> | |
... lines 14 - 15 | |
<label for="exampleInputPassword1">Password</label> | |
<input type="password" name="_password" class="form-control" | |
id="exampleInputPassword1" placeholder="Password"> | |
... lines 19 - 20 | |
<label for="the-answer">The answer to life the universe and everything</label> | |
<input type="text" name="the_answer" class="form-control" id="the-answer"> | |
... lines 23 - 25 | |
<input type="checkbox" name="terms"> I agree to want to be logged in | |
... lines 27 - 28 | |
<button type="submit" class="btn btn-default">Login</button> | |
</form> | |
... lines 31 - 32 |
It has a username field, a password field and then a couple of extra fields. One of them is "the answer to life the universe and everything", which of course must be set to 42. The user also needs to check this "agreement" checkbox.
So how can we make a login system that checks four fields instead of just the usual username and password? Hello Guard.
Before coding, start the built-in web server, which is now bin/console server:run
:
bin/console server:run
Try out /login
.
To use guard, we need a new class. I'll create a new directory called Security
,
but that's not important. Call the class WeirdFormAuthenticator
.Next, make
this class implement GuardAuthenticatorInterface
or extend the slightly easier
AbstractGuardAuthenticator
:
... lines 1 - 2 | |
namespace AppBundle\Security; | |
... lines 4 - 10 | |
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator; | |
class WeirdFormAuthenticator extends AbstractGuardAuthenticator | |
{ | |
... lines 15 - 42 | |
} |
In PhpStorm, I'll open the generate menu
and select "Implement Methods". Select all of them, including start()
, which is
hiding at the bottom. Also move the start()
method to the bottom of the class:
it'll fit more thematically down there:
... lines 1 - 4 | |
use Symfony\Component\HttpFoundation\Request; | |
... line 6 | |
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; | |
use Symfony\Component\Security\Core\Exception\AuthenticationException; | |
use Symfony\Component\Security\Core\User\UserInterface; | |
use Symfony\Component\Security\Core\User\UserProviderInterface; | |
... lines 11 - 12 | |
class WeirdFormAuthenticator extends AbstractGuardAuthenticator | |
{ | |
public function getCredentials(Request $request) | |
{ | |
} | |
public function getUser($credentials, UserProviderInterface $userProvider) | |
{ | |
} | |
public function checkCredentials($credentials, UserInterface $user) | |
{ | |
} | |
public function onAuthenticationFailure(Request $request, AuthenticationException $exception) | |
{ | |
} | |
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) | |
{ | |
} | |
public function supportsRememberMe() | |
{ | |
} | |
public function start(Request $request, AuthenticationException $authException = null) | |
{ | |
} | |
} |
Your job is simple: fill in each of these methods.
Once we hook it up, the getCredentials()
method will be called on the authenticator
on every single request. This is your opportunity to collect the username, password,
API token or whatever "credentials" the user is sending in the request.
For this system, we don't need to bother looking for the credentials unless the user
is submitting the login form. Add an if statement: if $request->getPathInfo()
- that's
the current, cleaned URL - != /login
or !$request->isMethod('POST')
, then return
null
:
... lines 1 - 28 | |
public function getCredentials(Request $request) | |
{ | |
if ($request->getPathInfo() != '/login' || !$request->isMethod('POST')) { | |
return; | |
} | |
... lines 34 - 40 | |
} | |
... lines 42 - 101 |
When you return null
from getCredentials()
, no other methods will be called on
the authenticator.
Below that, return an array that contains any credential information we need. Add
a username
key set to $request->request->get('_username')
: that's the name of
the field in the form:
... lines 1 - 28 | |
public function getCredentials(Request $request) | |
{ | |
... lines 31 - 34 | |
return [ | |
'username' => $request->request->get('_username'), | |
... lines 37 - 39 | |
]; | |
} | |
... lines 42 - 101 |
Repeat that for password and answer
. In the form, answer's
name is the_answer
and the last is named terms
. Finish the array by fetching
the_answer
and terms
:
... lines 1 - 28 | |
public function getCredentials(Request $request) | |
{ | |
... lines 31 - 34 | |
return [ | |
'username' => $request->request->get('_username'), | |
'password' => $request->request->get('_password'), | |
'answer' => $request->request->get('the_answer'), | |
'terms' => $request->request->get('terms'), | |
]; | |
} | |
... lines 42 - 101 |
The keys on the right are obviously the names of the submitted form fields. The keys on the left can be anything: you'll see how to use them soon.
If getCredentials()
returns a non-null value, then getUser()
is called next.
I have a really simple User
entity that has basically just one field: username
:
... lines 1 - 11 | |
class User implements UserInterface | |
{ | |
... lines 14 - 20 | |
/** | |
* @ORM\Column(type="string") | |
*/ | |
private $username; | |
... lines 25 - 54 | |
} |
I'm not even storing a password. Whatever your situation, just make sure you have
a User
class that implements UserInterface
and a "user provider" for it that's
configured in security.yml
:
... lines 1 - 2 | |
security: | |
... lines 4 - 5 | |
providers: | |
my_entity_users: | |
entity: | |
class: AppBundle:User | |
... lines 10 - 31 |
See the $credentials
variable? That's equal to whatever you returned from getCredentials()
.
Set a new $username
variable by grabbing the username
key from it:
... lines 1 - 17 | |
class WeirdFormAuthenticator extends AbstractGuardAuthenticator | |
{ | |
... lines 20 - 42 | |
public function getUser($credentials, UserProviderInterface $userProvider) | |
{ | |
$username = $credentials['username']; | |
... lines 46 - 53 | |
} | |
... lines 55 - 99 | |
} |
Let's get crazy and say: "Hey, if you have a username that starts with an @
symbol,
that's not okay and you can't login". To fail authentication, return null:
... lines 1 - 42 | |
public function getUser($credentials, UserProviderInterface $userProvider) | |
{ | |
$username = $credentials['username']; | |
// this looks like a weird username | |
if (substr($username, 0, 1) == '@') { | |
return; | |
} | |
... lines 51 - 53 | |
} | |
... lines 55 - 101 |
Now, query for the User! We need the entity manager, so add a private $em
property
and generate the constructor. Type-hint it with EntityManager
:
... lines 1 - 5 | |
use Doctrine\ORM\EntityManager; | |
... lines 7 - 17 | |
class WeirdFormAuthenticator extends AbstractGuardAuthenticator | |
{ | |
private $em; | |
... lines 21 - 22 | |
public function __construct(EntityManager $em, RouterInterface $router) | |
{ | |
$this->em = $em; | |
... line 26 | |
} | |
... lines 28 - 99 | |
} |
Back in getUser()
, this is easy: return $this->em->getRepository('AppBundle:User')->findOneBy()
with username
equals $username
:
... lines 1 - 42 | |
public function getUser($credentials, UserProviderInterface $userProvider) | |
{ | |
$username = $credentials['username']; | |
... lines 46 - 51 | |
return $this->em->getRepository('AppBundle:User') | |
->findOneBy(['username' => $username]); | |
} | |
... lines 55 - 101 |
The job of getUser()
is to return a User object or null to fail authentication.
It's perfect.
If getUser()
does return a User, checkCredentials()
is called. This is where
you check if the password is valid or anything else to determine that the authentication
request is legitimate.
And surprise! The $credentials
argument is once again what you returned from getCredentials()
.
Let's check a few things. First, the user doesn't have a password stored in the database.
In my system, the password for everybody is the same: symfony3
. If it doesn't equal
symfony3
, return null
and authentication will fail:
... lines 1 - 55 | |
public function checkCredentials($credentials, UserInterface $user) | |
{ | |
if ($credentials['password'] != 'symfony3') { | |
return; | |
} | |
... lines 61 - 70 | |
} | |
... lines 72 - 101 |
Second, if the answer
does not equal 42, return null
. And finally, if the terms
weren't checked, you guessed it, return null
. Authentication will fail unless checkCredentials()
exactly returns true
. So return that at the bottom:
... lines 1 - 55 | |
public function checkCredentials($credentials, UserInterface $user) | |
{ | |
... lines 58 - 61 | |
if ($credentials['answer'] != 42) { | |
return; | |
} | |
if (!$credentials['terms']) { | |
return; | |
} | |
return true; | |
} | |
... lines 72 - 101 |
That's it! The last few methods handle what happens on authentication failure and
success. Both should return a Response
object, or null to do nothing and let the
request continue.
The $exception
passed to onAuthenticationFailure
holds details about what went
wrong:
... lines 1 - 72 | |
public function onAuthenticationFailure(Request $request, AuthenticationException $exception) | |
{ | |
... lines 75 - 79 | |
} | |
... lines 81 - 101 |
Was the user not found? Were the credentials wrong? Store this in the session
so we can show it to the user. For the key, use the constant: Security::AUTHENTICATION_ERROR
:
... lines 1 - 72 | |
public function onAuthenticationFailure(Request $request, AuthenticationException $exception) | |
{ | |
$request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception); | |
... lines 76 - 79 | |
} | |
... lines 81 - 101 |
This is exactly what the normal form login system does: it adds an error to this same key on the session.
In the controller, the security.authentication.utils
service reads this key from the session
when you call getLastAuthenticationError()
:
... lines 1 - 8 | |
class DefaultController extends Controller | |
{ | |
... lines 11 - 21 | |
/** | |
* @Route("/login", name="login") | |
*/ | |
public function sillyLoginAction() | |
{ | |
$error = $this->get('security.authentication_utils') | |
->getLastAuthenticationError(); | |
... lines 29 - 32 | |
} | |
... lines 34 - 40 | |
} |
Other than storing the error, what we really want to do when authentication fails
is redirect back to the login page. To do this, add the Router
as an argument to
the constructor and use that to set a new property. I'll do that with a
shortcut:
... lines 1 - 9 | |
use Symfony\Component\Routing\RouterInterface; | |
... lines 11 - 17 | |
class WeirdFormAuthenticator extends AbstractGuardAuthenticator | |
{ | |
... line 20 | |
private $router; | |
public function __construct(EntityManager $em, RouterInterface $router) | |
{ | |
... line 25 | |
$this->router = $router; | |
} | |
... lines 28 - 99 | |
} |
Now it's simple: $url = $this->router->generate()
and pass it login
- that's the
route name to my login page. Then, return a new RedirectResponse
:
... lines 1 - 72 | |
public function onAuthenticationFailure(Request $request, AuthenticationException $exception) | |
{ | |
... lines 75 - 76 | |
$url = $this->router->generate('login'); | |
return new RedirectResponse($url); | |
} | |
... lines 81 - 101 |
When authentication works, keep it simple and redirect back to the homepage. In a real app, you'll want to redirect them back to the previous page:
... lines 1 - 81 | |
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) | |
{ | |
$url = $this->router->generate('homepage'); | |
return new RedirectResponse($url); | |
} | |
... lines 88 - 101 |
There's a base class called AbstractFormLoginAuthenticator
that can help with this.
The start()
method is called when the user tries to access a page that requires
login as an anonymous user. For this situation, redirect them to the login page:
... lines 1 - 88 | |
public function start(Request $request, AuthenticationException $authException = null) | |
{ | |
$url = $this->router->generate('login'); | |
return new RedirectResponse($url); | |
} | |
... lines 95 - 101 |
Finally, if you want to be able to support remember me functionality, return true
from supportsRememberMe()
:
... lines 1 - 95 | |
public function supportsRememberMe() | |
{ | |
return true; | |
} | |
... lines 100 - 101 |
You'll still need to configure the remember_me
key in the firewall.
That's it! Now we need to tell Symfony about the authentication with two steps. The
first shouldn't surprise you: register this as a service. In services.yml
create
a new service - how about weird_authenticator
. The class is WeirdFormLoginAuthenticator
and there are two arguments: the entity manager and the router:
... lines 1 - 5 | |
services: | |
weird_authenticator: | |
class: AppBundle\Security\WeirdFormAuthenticator | |
arguments: ['@doctrine.orm.entity_manager', '@router'] |
Finally, hook this up in security.yml. Under your firewall, add a guard
key and
an authenticators
key below that. Add one authenticator - weird_authenticator
-
the service name:
... lines 1 - 2 | |
security: | |
... lines 4 - 10 | |
firewalls: | |
... lines 12 - 16 | |
main: | |
anonymous: ~ | |
... lines 19 - 21 | |
guard: | |
authenticators: | |
- weird_authenticator | |
... lines 25 - 31 |
Now, getCredentials()
will be called on every request. If it returns something
other than null
, getUser()
will be called. If this returns a User
, then
checkCredentials()
will be called. And if this returns true
, authentication
will pass.
Try it out! With a blank form, it says Username could not be found
. It is looking
for the credentials on the request, but it fails on the getUser()
method. I do
have one user in the database: weaverryan
. Put symfony3
for the password but
set the answer to 50. This fails with "Invalid Credential". Finally, fill in everything
correctly. And it works! The web debug toolbar congratulates us: logged in as weaverryan.
We saw two different error messages, depending on whether authentication failed in
getUser()
or checkCredentials()
. But you can completely control these messages
by throwing a CustomUserMessageAuthenticationException()
. I know, it has a long name,
but it's job is clear: pass it the message you want want to show the user, like:
... lines 1 - 12 | |
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; | |
... lines 14 - 43 | |
public function getUser($credentials, UserProviderInterface $userProvider) | |
{ | |
... lines 46 - 48 | |
if (substr($username, 0, 1) == '@') { | |
throw new CustomUserMessageAuthenticationException( | |
'Starting a username with @ is weird, don\'t you think?' | |
); | |
} | |
... lines 54 - 56 | |
} | |
... lines 58 - 108 |
Below if the answer
is wrong, do the same thing: throw new CustomUserMessageAuthenticationException()
with:
... lines 1 - 58 | |
public function checkCredentials($credentials, UserInterface $user) | |
{ | |
... lines 61 - 64 | |
if ($credentials['answer'] != 42) { | |
throw new CustomUserMessageAuthenticationException( | |
'Don\'t you read any books?' | |
); | |
} | |
... lines 70 - 77 | |
} | |
... lines 79 - 108 |
And in case the terms checkbox isn't checked, throw a new CustomUserMessageAuthenticationException
and threaten them to agree to our terms!
... lines 1 - 58 | |
public function checkCredentials($credentials, UserInterface $user) | |
{ | |
... lines 61 - 70 | |
if (!$credentials['terms']) { | |
throw new CustomUserMessageAuthenticationException( | |
'Agree to our terms!!!' | |
); | |
} | |
return true; | |
} | |
... lines 79 - 108 |
Try it out with @weaverryan
. There's the first message! Try 50 for the answer:
there's the second message. And if you fill in everything right but have no checkbox,
you see the final message.
That's Guard authentication: create one class and do whatever the heck that you want.
I showed a login form, but it's even easier to use for API authentication. Basically,
authentication is not hard anymore. Invent whatever insane system you want and give
the user exactly the message you want. If you want to return JSON
on authentication
failure and success instead of redirecting, awesome, do it.
I solved the problem with encoded password, I made mistake in my fixtures and I was encoding password not plain password. But I still can't find out why when i log in in toolbar I see
Loggedin: admin
Authenticated: No
Hey Andrzej!
I'm glad you solved the first problem :). About the second problem, what does your getRoles() method look like in your User class? Make sure that it *always* returns at least *one* role. If you return zero roles from getRoles(), then you will see this behavior. Let me know if that's the problem!
Cheers!
Hello, this is my getRoles() method
public function getRoles()
{
$roles = $this->roles;
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
Here is screen from my toolbar
http://www.awesomescreenshot.com/image/1101770/f621f2898398db69d11601314f1bd26c
Hmm, curious - that is *not* the problem then. Does your user implement AdvancedUserInterface by chance?
I found the reason of this problem, it was cause because of setPlainPassword() and eraseCredentials() methods in User class. I found in Symfony documentation that it is good co clear plainPassword for security reasons. Commenting out $this->setPlainPassword(null); fixes the problem.
public function setPlainPassword($plainPassword)
{
$this->plainPassword = $plainPassword;
$this->setPassword(null);
}
public function eraseCredentials()
{
$this->setPlainPassword(null);
}
Hi Andrzej!
Yes, it *is* good to clear your plan password in eraseCredentials - it just makes sure that the plain password doesn't accidentally get stored anywhere, like the session. Symfony automatically calls this after you login. You just need to make sure that you don't also clear the password field :). In this case, in eraseCredentials(), I would directly set $this->plainPassword = null - instead of calling setPlainPassword(). That should also fix the problem.
Cheers and thanks for sharing!
Hi, I just posted a comment because I have the same problem (did this this sorry). But the eraseCredential don't seems to be the problem (with comment or setPlainPassword = null, the problem is still here)
Hi everyone !
Thanks for guard, it's really a nice way to make authentication simpler.
I have a question about it though, i'm trying to use a form type as login form, but i can't figure out where the form should be validated ? At start of the guard class in getCredentials() ? If so, how would i be able to send back the error list of the form ?
Is it a bad practice to go for a form type for login form ?
If i use a chain of guard authenticators, would the form need to be validated on every getCredential() ?
Thanks in advance if any of you have answer / advice about this.
And thanks again for those nice tutorials :)
Hi there!
Really good questions :). Let me try to help:
1) I don't usually use a Symfony form for a login form - I feel it's overkill. But, it's certainly not wrong.
2) Yes, I would submit the form in getCredentials(), and then return the array of data from that method (or an object, if you have your form bound to an object).
3) How to send back the errors, is a little more difficult. If the form is not valid, I would probably save the form into the request object and then fail authentication (all in getCredentials()):
if (!$form->isValid()) {
$request->attributes->set('login_form', $form);
}
Then, in onAuthenticationFailure(), I would do nothing: allow the request to continue to your controller. Make sure that the URL you submit your form to (e.g. /login) is the same URL that displays your login form. That way, by doing nothing in onAuthenticationFailure, the request will continue and will call your loginAction (the one that displays your form). Here, look for the Form in the Request and use it:
public function loginAction(Request $request)
{
if ($request->attributes->has('login_form')) {
$form = $request->attributes->get('login_form');
$request->attributes->remove('login_form');
} else {
$form = $this->createForm(...);
}
// ... all your normal logic
}
If authentication fails, you will use the form that was stored (temporarily) on the request: this will have all the normal, bound errors.
For me - this isn't worth the trouble :). But it certainly will work just fine.
4) If you have a chain of authenticators, each of them should be authenticating in a different way - e.g. one for form login, one for API key authentication etc. So, think only your "form login" authenticator would have a form - the others would just do whatever they need to do.
I hope that helps!
Cheers!
Hi there there I think this is already bundle in latest Symfony 2.8 and 3.0 I am right? Also I would like to know if this component could help me to achieve what I am trying to do. Can you take a look to this post: How to create “two” (chat and admin) or more secured areas with FOSUserBundle (http://stackoverflow.com/qu... in SO and tell me if this can be achieved with Guard?
I'm a bit confused why you load the Entity Manager if you already have the user provider.
Anyway, small question, I'm building a symfony app that will load data and user form an API, and I need to send a token (JWT) to the API for every request.
So here, I would need 2 guards authentication, 1 for a form, where I can send login and pass to authenticate and receive my token from the api, then store it in a cookie, and a 2nd one, that will check my cookies for the token and authenticate if it's here?
I got a bit confused, not sure it's the best solution.
Thank you! :)
Hey arvilefvre!
That's a *great* question. It's mostly because... I hate the user provider :). Basically, the user provider should be less coupled into Symfony's security system - it actually only serves 2 purposes: (A) to help refresh your user from the sessions and (B) to load the user for the rememeber_me of for the switch_user functionality. In theory, you should not even *need* a user provider if you were creating a pure API (with no sessions).
So, when I teach security, I try to ignore the user provider as much as possible, because it's much less important than many people think (and mostly confusing).
Now, about your real question! You will definitely need a Guard authenticator for handling the incoming JWT token. Though, it's more common for you to send the token on a header, rather than read it on a cookie. Why? Because if you rely on cookies, then basically the only "thing" that can use your API is your own JavaScript. And if you're building an API *only* for your own JavaScript, you could just rely on normal session cookies and not mess with *any* API token stuff (just have the user login normally, which sets a session cookie, and then start making AJAX requests).
But anyways, if you *do* still need the full setup, then, for the first part (handling the login & pass and then sending back an API token), I would do this in a *controller* not an authenticator (though you could do it either way)? Why? The purpose of an authenticator is to *authenticate* your user. But if the purpose of your endpoint is to simply check their username/password and send *back* an authentication token, then you probably don't actually need to *authenticate* the user on that request. This situation is less about security, and it's more a pure API endpoint where the user is sending some data, and you're sending some data back. We do this exact thing in another tutorial: https://knpuniversity.com/s...
I hope that helps! Cheers!
Hi!
Thx for your quick answer :)
As for my symfony app, I guess I was not so clear, but It's actually a front web app that need to connect to another api, so I need to get a login and password from the user, send them to the api, that will send back to me a token that I need to keep to be able to call the api afterward.
I managed to make it work (thx to your article, helped me to understand the guard system better :)), I'm not sure it's the best way, but it works ^^
Hi!
Awesome tutorial as always. I'm struggling with something that might be so easy to do that I'm totally missing the point and I'm going crazy here!
Basically, when the user logs in successfully and the onAuthenticationSuccess() method is called, how do I pass the user object to my userController.php? I want to achieve 2 things in here:
1- have the route in my controller as /user/{user_id}/dashboard
2- use the shooter details in the twig template
My onAuthenticationSuccess() looks like this:
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
$targetPath = $this->getTargetPath($request->getSession(), $providerKey);
if (!$targetPath)
{
$targetPath = $this->router->generate('user_dashboard');
}
return new RedirectResponse($targetPath);
}```
My dashBoardAction() on my userController.php looks like this:
/**
* @Route("/user/{id}/dashboard", name="user_dashboard")
*/
public function dashboardAction(WhatDoICallHere $whatDoICallHere)
{
return $this->render('users/dashboard.html.twig', [
'user' => $noIdeaWhatToPutHere,
]);
}```
Thanks!
Hey Javier,
I think you can get current user from the $token like $token->getUser(), and then you'll be able to generate the "user_dashboard" URL in onAuthenticationSuccess().
In your controller, if you extends Symfony default controller which is "Symfony\Bundle\FrameworkBundle\Controller\Controller" - you have a shortcut to get current user object, just call $this->getUser() in your dashboardAction().
Cheers!
Hi Victor,
I've tried the following:
LoginFormAuthenticator.php:
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
$targetPath = $this->getTargetPath($request->getSession(), $providerKey);
$user = $token->getUser();
if (!$targetPath)
{
$targetPath = $this->router->generate('user_dashboard');
}
return new RedirectResponse($targetPath, [
'user' => $user,
]);
}```
UserController.php:
/**
* @Route("/user/{id}/dashboard", name="user_dashboard")
*/
public function dashboardAction(Request $request)
{
$user = $this->getUser();
return $this->render('users/dashboard.html.twig', [
'user' => $user,
]);
}```
However, I get the following error:
Some mandatory parameters are missing ("id") to generate a URL for route "user_dashboard".```
Any ideas?
Thanks.
Hey Javier,
Haha, of course! This error makes perfect sense ;) You have to pass User ID when you generate user_dashboard URL:
$user = $token->getUser();
$targetPath = $this->router->generate('user_dashboard', [
'id' => $user->getId(),
]);
And btw, you don't have to pass user to the RedirectResponse, just return:
return new RedirectResponse($targetPath);
Very cool tut' !
Is it possible to use Guard with a CAS Server ?... Is there a ressource on the net I can read ?...
Thank you
Hi Abou!
Absolutely! But, it might even be easier to use this bundle without guard: https://github.com/BeSimple.... You can see an example project created by a friend of mine here that uses this for CAS: https://github.com/rlbaltha....
Cheers!
Thank's a lot for the answer !
I've tried to use that bundle but composer complains for some incompatibility with my symfony version (3.1.4) ... :(
Anyway thank you so much for beeing so envolved and so kind to answer all ower questions !
Abou.
Ah, you're right! What a bummer! Well, hopefully you can find something else or build off of its ideas at least!
Cheers!
Hi Ryan! I really love the guard component, it has helped make security in my app much simpler to set up and maintain. One of my guard authenticators is a traditional form login. Right now, when someone is redirected to the login page after trying to reach a secure page, I have the onAuthenticationSuccess method redirect them to the index page of the secured area. How can I direct them to the page the requested before logging in?
Hey David L. ,
If I recall correctly, you should return null in your `onAuthenticationSuccess()` and it will work out of the box. Could you try it? If it still does not work, force it with enabling use_referer option. I think it should help.
Cheers!
It didnt work, I just sit at the login page now. Here is what my main firewall looks like. I didnt have the form_login section before, i added it so I could have the use_referer option. Without that, I got an error when the controller returned null.
main:
pattern: ^/
anonymous: ~
logout:
path: /logout
target: /
form_login:
use_referer: true
login_path: /admin/login
check_path: /admin/login_check
guard:
provider: admin_user_provider
authenticators:
- app.authenticator.form
Yes, this is a really important detail - one that we use here on our site!
Here's the trick: use the TargetPathTrait and call the getTargetPath() method: https://github.com/symfony/.... This is new in Symfony 3.1 - but no problem: if you're on an earlier version, simply copy that small function into your code.
Here's what's going on:
A) When you try to access some secure page (e.g. /admin) and are redirected to the login page (or more accurately, the start() method in one of your authenticators redirects the user to the login page), Symfony automatically stores this URL (/admin) in the session under a very specific key.
B) The TargetPathTrait::getTargetPath() function is just a helper for you to read this key from the session.
Let me know if it works! We also do this same thing after registration - since the user might choose to register instead of logging in ;).
Cheers!
Hello thanks for this tutorial and this awesome tool. I'm using Guard in my project and i
wanted to add remember me feature i made the supportRemeberme method
return true and configured the remember me firewall as mentioned in the
symfony documentation and set a checkbox with as name the remember_me
parameter but the cookie is not set and the feature is not working. Looking for some hep to make things work. thank you
Hey Ali Karchoud!
Hmm. Try changing the checkbox name to _remember_me
- that's what it should be. Otherwise, if it still doesn't work, if you can push a test repository to GitHub with the issue, I can take a look.
Cheers!
ty sir for the answer i already have the checkbox with that name which is the default remember_me parameter.
well since the project i'm working on is a company project i can't publish it on github and i don't have access to private repo but i can give u the code snippets from authenticator and the firewall and the view
Yo Ali Karchoud!
Here's what we can do :). Create a fork of https://github.com/symfony/... and add your authenticator and security configuration there to repeat the problem. Then, you can share that repository with me :). If you only give me the form authenticator, firewall and view code, then *I* will need to take the time to integrate it into the symfony-demo to try to repeat the problem. So if you can do that for me, I can return the favor by taking some time to help debug the issue.
Cheers!
Ty sir i appreciate your help I’m busy at the moment so I will do that later and share with you the link. What you are offering is a real favor weaverryan
Trying to figure out how to check a user against LDAP inside of checkCredentials in a custom authenticator (in my case
LoginFormAuthenticator). I have a feeling this must be easy, since the form_login_ldap does it, but I can't discern the right way to do it.
I would use the form login ldap authenticator, except that in my case I need some more logic, where I first find the user in a local database, then based on the App type of the user, either auth against the database password, or auth against an external LDAP server.
What would I use in place of "unknownLdapService" below? Or is there a different way to do this?
public function checkCredentials($credentials, UserInterface $user)
{
...
// check password if the user is database user
if ($user->getApp() == 'DB') {
if ($this->passwordEncoder->isPasswordValid($user, $password)) {
return true;
}
}
// check LDAP server if LDAP user
if ($this->getApp() == 'LDAP') {
if ($this->unknownLdapService->check($user, $password)
{
return true;
}
Hey Geoff Maddock!
This is a perfect use-case for Guard authentication :). Ok, about your question. For LDAP, you probably already sent the credentials to LDAP inside getUser() in order to authenticate and get the user information, correct? If that's true, then you've effectively already checked the user's credentials then. This means in checkCredentials(), you can just return true.
Fun fact: you could always do everything in getUser()
and just hardcode a return true
in checkCredentials(). We made 2 separate methods just to help "guide" you. And also, the user will get a slightly different error message based in which method authentication fails in... but you can also throw a CustomUserMessageAuthenticationException to create your own custom message from anywhere :).
Let me know if this helps!
Cheers!
Thanks for the followup. In my case, I did not query LDAP in getUser, just the User entity repo. In my case, all users (usernames) are in the User Entity database. So I don't have to get them from the LDAP server. But I do need to validate the passwords of some of the users against LDAP.
So can I inject the LDAP class (Symfony\Component\Ldap\Ldap) into the LoginFormAuthenticator constructor and then build and call the bind there? Or is there another service that makes more sense to use?
Attempting to use use (Symfony\Component\Ldap\Ldap), inside of the checkCredentials such as:
...
// build the correct username
$login_username = sprintf('DOMAIN\%s', $user);
$username = $this->ldap->escape($user, '', LdapInterface::ESCAPE_FILTER);
// check LDAP server if LDAP user
if ($this->getApp() == 'LDAP') {
// if the bind fails, this throws 500 - Invalid credentials
$this->ldap->bind($login_username, $password)
// if the bind succeeds, i can return true since it successfully authed
return true;
}
...
Interestingly in this case, if the username and password don't match, it throws a 500 error and states "Invalid Credentials", which seems right although I'd like to catch that error. So that does work, but doesn't seem like the right way of doing things.
I saw LdapBindAuthenticationProvider but it wasn't clear how I'd call that.
Yo Geoff Maddock!
Ah, ok - it's much more clear now. And I think you're quite close. I haven't used Symfony's Ldap component directly yet, so some of my recommendations might not be quite right. But yes, I *believe* that using this Ldap class is the correct one. And if I'm reading the code correctly, you should catch this exception: https://github.com/symfony/...
On the catch, just return false. It may seem weird, but that class is designed to throw an exception in case binding fails. So, we *do* need to catch it and return false so that the proper flow can go forward. The LdapBindAuthenticationProvider is buried too deep in the ldap security system for us to use it from Guard. But... check this out! They *also* use bind() inside a try-catch: https://github.com/symfony/.... I'd say that 100% validates your approach :).
Cheers!
Awesome, thanks for the assist. I think I have my authenticator working the way I need it to work (including re-encrypting old sha1 passwords automatically on login).
One last, and merely tangential question - what's the best practice for accessing a parameter from a class like my custom LoginFormAuthenticator? I'd like to define the format for my LDAP login somewhere globally, like in parameters.yml, but I won't be able to access that value without the container.
Hey Geoff Maddock!
Awesome! Nice work!
About your question, If I understand it correctly, you can pass parameters through your __construct function just like any services. The only differences is that parameters (or any scalar values) can't be autowired, so you'll need to wire them up manually - see https://knpuniversity.com/screencast/symfony-3.3/named-arguments.
Oh, and I'd recommend only putting that config in parameters.yml if the format will change from server to server (e.g. it's different locally versus production, like a database password). If it does not change, and you just want to avoid hardcoding it in your class, then add it as a parameter in services.yml
. Then, it will be committed to the repository and you won't need to worry about managing that config separately on all servers.
Cheers!
Hi Ryan,
Guard was super helpful with one of my legacy projects. I never managed to do a "silent" migration from old md5 hashed passwords to bcrypt encoded passwords. Maybe I have not tried hard enough - anyway: With Guard it was a piece of cake. In the checkCredentials I did just this:
$plainPassword = $credentials['password'];
if(null === $user->getPassword()) {
if($user->getOldPassword() === md5($plainPassword.$user->getOldSalt())) {
$user
->setPassword($this->encoder->encodePassword($user, $plainPassword))
->setOldPassword(null);
;
$this->em->persist($user);
$this->em->flush();
}
}
if (!$this->encoder->isPasswordValid($user, $plainPassword)) {
throw new BadCredentialsException();
}
return true;
Thought that might be helpful for others having the same problem.
I have also a question: I tried to use the impersonation featured as described in another tutorial https://knpuniversity.com/screencast/symfony-security/impersonation-switch-user , but it does not work. I am trying to impersonate an admin (logged in behind admin firewall) as employee (behind an employee firewall). Is impersonation supposed to work the way as described in this other screencast mentioned above? Am I doing it wrong usinge different firewalls for that? Or something else?
Hey Thomas Baumgartner!
Ah, awesome! Thanks for sharing this solution - it's very clever!
About the impersonation, it should just work - it's independent of whether you're using Guard or some other authentication system. There are two things you should look at: (A) Make sure you have the switch_user
key configured under your firewall and (B) make sure you "user provider" is setup correctly (since the loadUserByUsername
method is used to actually find and load the new user when you switch).
If that doesn't help, let me know the error - it may spark another idea :).
Cheers!
Thank you Ryan!
I'm sorry to have bothered you with that. Turned out that it's not an impersonation problem, but has rather something to do with firewalls / access_control / role_hierarchy. I also have different routes for admin_login / employee_login, same with login_check and logout, but I am using the same UserProvider and database table for both. Now the strange thing is that I am able to log in into both araeas of the site with the same admin user (as for the role_hierarchy is granting me this), but I am not automatically logged in as employee, when I log into the admin area. (and I think thats the reason why impersonation cannot work either). So the question is, what makes the tokenStorage to be restricted to one area/acl-path only? (Interestingly, if I log out from one of the areas, I am logged out from both). Below my security.yml:
firewalls:
admin:
pattern: ^/admin/
anonymous: ~
guard:
authenticators:
- app.admin_login_authenticator
logout:
path: admin_logout
target: admin_login
employee:
pattern: ^/employee/
anonymous: ~
guard:
authenticators:
- app.employee_login_authenticator
logout:
path: employee_logout
target: employee_login
switch_user: ~
access_control:
- { path: "^/admin/login", roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: "^/employee/login", roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: "^/admin/", roles: [ROLE_ADMIN, ROLE_PROJECT_MANAGER] }
- { path: "^/employee/", roles: [ROLE_EMPLOYEE] }
role_hierarchy:
ROLE_DEVELOPER: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
ROLE_ADMIN: [ROLE_PROJECT_MANAGER, ROLE_ALLOWED_TO_SWITCH]
ROLE_PROJECT_MANAGER: [ROLE_EMPLOYEE]
ROLE_EMPLOYEE: []
UPDATE: hah, got it! It's the firewall configuration. Not sharing the token between firewalls totally makes sense, just not in my case. I only had to add a context property containing the same string to each of my firewall configuartions and I'm done. Token shared, impersonation works. Thank you so much again Ryan, feel free to delete this post, if you consider it too much off topic in this place.
Great find and fix Thomas! Yes, multiple firewalls don't share *anything*... unless you find that nice context property :). Cheers!
Hi Ryan i have a probleme with my authentication it worked perfectly but today i make some modification in the front end and now when i login its working but immediately after connexion the logout url is called automatically and i am logged out.
I am very confused!!!!
Help please
// composer.json
{
"require": {
"php": ">=5.5.9",
"symfony/symfony": "3.0.*", // v3.0.0
"doctrine/orm": "~2.5@dev", // 2.7.x-dev
"doctrine/doctrine-bundle": "~1.6@dev", // 1.10.x-dev
"doctrine/doctrine-cache-bundle": "~1.2@dev", // 1.3.2
"symfony/security-acl": "~3.0@dev", // dev-master
"symfony/swiftmailer-bundle": "~2.3", // v2.3.8
"symfony/monolog-bundle": "~2.7@dev", // dev-master
"sensio/distribution-bundle": "~5.0@dev", // v5.0.22
"sensio/framework-extra-bundle": "^3.0.2", // v3.0.11
"incenteev/composer-parameter-handler": "~2.0", // v2.1.2
"doctrine/doctrine-fixtures-bundle": "^2.3", // v2.4.1
"composer/package-versions-deprecated": "^1.11" // 1.11.99
},
"require-dev": {
"sensio/generator-bundle": "~3.0", // v3.0.0
"symfony/phpunit-bridge": "~2.7" // v2.7.6
}
}
Hello,
I am implementing custom authentication with guard end everything works fine until I create password encoding. To encode password I use doctrine event listeners (I pass @security.password_encoder as a servcie argument) and I encode password this way
And then inside FormLoginAuthenticator I check password in checkCredentials method
And here i have the problem becasue it alwys return false. I tried to expermient with password encoding and i did dump() of
$this->userPasswordEncoder->encodePassword($user, 'admin123'); a couple of times and it always returned different string. I wonder what I am doing wrong?
When I get rid of password encoding and i save plain password in datase authentication works fine, except the fact that in symfony toolbar i see:
Loggedin: admin
Authenticated: No
What might be the reason of this?