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 SubscribeGo back to the HTML form: it has one other field that we haven't talked about yet: the "remember me" checkbox:
... lines 1 - 10 | |
{% block body %} | |
<form class="form-signin" method="post"> | |
... lines 13 - 26 | |
<div class="checkbox mb-3"> | |
<label> | |
<input type="checkbox" value="remember-me"> Remember me | |
</label> | |
</div> | |
... lines 32 - 34 | |
</form> | |
{% endblock %} |
You could check & uncheck this to your heart's delight: that works great. But... checking it does... nothing. No worries: making this actually work is super easy - just two steps.
First, make sure that your checkbox has no value and that its name is _remember_me
:
... lines 1 - 10 | |
{% block body %} | |
<form class="form-signin" method="post"> | |
... lines 13 - 26 | |
<div class="checkbox mb-3"> | |
<label> | |
<input type="checkbox" name="_remember_me"> Remember me | |
</label> | |
</div> | |
... lines 32 - 34 | |
</form> | |
{% endblock %} |
That's the magic name that Symfony will look for. Second, in security.yaml
, under your firewall, add a new remember_me
section. Add two other keys below this. The first is required: secret
set to %kernel.secret%
:
security: | |
... lines 2 - 8 | |
firewalls: | |
... lines 10 - 12 | |
main: | |
... lines 14 - 22 | |
remember_me: | |
secret: '%kernel.secret%' | |
... lines 25 - 40 |
Second, lifetime
set to 2592000, which is 30 days in seconds:
security: | |
... lines 2 - 8 | |
firewalls: | |
... lines 10 - 12 | |
main: | |
... lines 14 - 22 | |
remember_me: | |
secret: '%kernel.secret%' | |
lifetime: 2592000 # 30 days in seconds | |
... lines 26 - 40 |
This option is... optional - it defaults to one year.
As soon as you add this key, if the user checks a checkbox whose name is _remember_me
, then a "remember me" cookie will be instantly set and used to log in the user if their session expires. This secret
option is a cryptographic secret that's used to sign the data in that cookie. If you ever need a cryptographic secret, Symfony has a parameter called kernel.secret
. Remember: anything surrounded by percent signs is a parameter. We never created this parameter directly: this is one of those built-in parameters that Symfony always makes available.
To see a list of all of the parameters, don't forget this handy command:
php bin/console debug:container --parameters
The most important ones start with kernel
. Check out kernel.secret
. Interesting, it's set to %env(APP_SECRET)%
. This means that it's set to the environment variable APP_SECRET
. That's one of the variables that's configured in our .env
file.
Anyways, let's try this out! I'll re-open my inspector and refresh the login page. Go to Application, Cookies. Right now, there is only one: PHPSESSID
.
This time, check the "remember me" box and log in. Now we also have a REMEMBERME
cookie! And, check this out: I'm logged in as spacebar1@example.com
. Delete the PHPSESSID
- it currently starts with q3
- and refresh. Yes! We are still logged in!
A totally new session was created - with a new id. But even though this new session is empty, the remember me cookie causes us to stay logged in. You can even see that there's a new Token class called RememberMeToken
. That's a low-level detail, but, it's a nice way to prove that this just worked.
Next - we've happily existed so far without storing or checking user passwords. Time to change that!
Hey Benoit L.
Adding the remember_me config to the security.yaml file should be enough for it to work. Are you using a special browser or browsing in incognito mode?
Cheers!
No, i set expiry to one month, but when I delete the PHPSESSION and refresh i am back to the login page?
Hey Benoit L.!
Hmmm. So here's the first thing to check. Before you delete the PHPSESSID cookie (that was a good thing to try) look and make sure that there is ALSO a cookie called REMEMBERME
. If there is not, then there is a problem setting the REMEMBERME cookie. If that cookie does exist, double-check its expiration in your browser to make sure it's one month from now. If it's not, then we know there is an expiration problem. If it is... well... then let me know and we'll go from there ;).
Cheers!
Hello Ryan. I am completly there. I checked all of your recommendations. REMEMBERME Cookie is there - expiration is ok. But I get logged out on page refresh after I delete the session cookie. Can your help, pleeeease?
Hey Michael B.!
Just for others' reference, you found your problem and we're tracking it over here (it's related to username vs email and the user provider): https://symfonycasts.com/sc...
Cheers!
So I checked, cookie is here, its date is one month forward, so it's not an expiration problem. Maybe you can give me the entry point to debug and find out.
Hey Benoit L.!
So I checked, cookie is here, its date is one month forward, so it's not an expiration problem
Great, so we can eliminate several things.
Maybe you can give me the entry point to debug and find out.
Happy to :). By the way, the security system logs a lot of messages through the process. So, if you remove the session cookie, tail -f var/log/dev.log
then refresh the page, you might get some "hints" about what's going wrong.
Here is the whole process: when you reload the page and the session cookie is missing, it (obviously) means that the security system will not be able to authenticate you. Then, still at the beginning of the request, the "remember me" system looks for the REMEMBEREME
cookie and tries to work its magic. Here is what that process looks like... and you can debug at each step:
1) The security system calls RememberMeListener::__invoke() https://github.com/symfony/symfony/blob/74d5fa62ceb8f3c2f60068a94551426424f39381/src/Symfony/Component/Security/Http/Firewall/RememberMeListener.php#L60
2) The first thing this does is call $this->rememberMeServices->autoLogin($request)
, which is what reads the cookie and attempts to query for the User object. You can find the code for this here: https://github.com/symfony/symfony/blob/74d5fa62ceb8f3c2f60068a94551426424f39381/src/Symfony/Component/Security/Http/RememberMe/AbstractRememberMeServices.php#L95 and here https://github.com/symfony/symfony/blob/74d5fa62ceb8f3c2f60068a94551426424f39381/src/Symfony/Component/Security/Http/RememberMe/TokenBasedRememberMeServices.php#L32 - ultimately the autoLogin
method should return a RememberMeToken
with the User object inside of it.
3) The RememberMeListener then calls $this->authenticationManager->authenticate($token)
. This just does a few last checks and once again returns a RememberMeToken with the User object inside. You can find that code here: https://github.com/symfony/symfony/blob/74d5fa62ceb8f3c2f60068a94551426424f39381/src/Symfony/Component/Security/Core/Authentication/Provider/RememberMeAuthenticationProvider.php#L40
4) And... that's it! The token is set into the "token storage"... which means you're now authenticated.
Let me know if you can see if any of this process is failing... or what you're seeing in the logs.
Cheers!
Hello, thanks for the info, first I made the authenticator from scratch, not using FOSUserBundle to understand better the authentication system of Symfony. I didn't follow the lesson on SymfonyCast neither. Here are some new insights in the logs :
security.DEBUG: User was reloaded from a user provider. {"provider":"Symfony\\Bridge\\Doctrine\\Security\\User\\EntityUserProvider","username":"yvon.huynh"} []
[2019-09-16 10:30:03] security.DEBUG: Checking for guard authentication credentials. {"firewall_key":"main","authenticators":1} []
[2019-09-16 10:30:03] security.DEBUG: Checking support on guard authenticator. {"firewall_key":"main","authenticator":"App\\Security\\LoginFormAuthenticator"} []
[2019-09-16 10:30:03] security.DEBUG: Guard authenticator does not support the request. {"firewall_key":"main","authenticator":"App\\Security\\LoginFormAuthenticator"} []
[2019-09-16 10:30:10] security.DEBUG: Stored the security token in the session. {"key":"_security_main"} []
[2019-09-16 10:30:11] request.INFO: Matched route "_wdt". {"route":"_wdt","route_parameters":{"_route":"_wdt","_controller":"web_profiler.controller.profiler::toolbarAction","token":"ded20b"},"request_uri":"http://geothermev2/_wdt/ded20b","method":"GET"} []
[2019-09-16 10:30:11] request.ERROR: Uncaught PHP Exception Symfony\Component\HttpKernel\Exception\NotFoundHttpException: "No route found for "GET /favicon.ico" (from "http://geothermev2/dashboard")" at /Users/poste5hookipa/sites/geothermeV2/vendor/symfony/http-kernel/EventListener/RouterListener.php line 141 {"exception":"[object] (Symfony\\Component\\HttpKernel\\Exception\\NotFoundHttpException(code: 0): No route found for \"GET /favicon.ico\" (from \"http://geothermev2/dashboard\") at /Users/poste5hookipa/sites/geothermeV2/vendor/symfony/http-kernel/EventListener/RouterListener.php:141, Symfony\\Component\\Routing\\Exception\\ResourceNotFoundException(code: 0): No routes found for \"/favicon.ico/\". at /Users/poste5hookipa/sites/geothermeV2/vendor/symfony/routing/Matcher/Dumper/CompiledUrlMatcherTrait.php:70)"} []
[2019-09-16 10:30:21] request.INFO: Matched route "dashboard". {"route":"dashboard","route_parameters":{"_route":"dashboard","_controller":"App\\Controller\\MainController::index"},"request_uri":"http://geothermev2/dashboard","method":"GET"} []
[2019-09-16 10:30:21] security.DEBUG: Checking for guard authentication credentials. {"firewall_key":"main","authenticators":1} []
[2019-09-16 10:30:21] security.DEBUG: Checking support on guard authenticator. {"firewall_key":"main","authenticator":"App\\Security\\LoginFormAuthenticator"} []
[2019-09-16 10:30:21] security.DEBUG: Guard authenticator does not support the request. {"firewall_key":"main","authenticator":"App\\Security\\LoginFormAuthenticator"} []
[2019-09-16 10:32:05] security.DEBUG: Remember-me cookie detected. [] []
[2019-09-16 10:32:05] doctrine.DEBUG: SELECT t0.id AS id_1, t0.firstname AS firstname_2, t0.lastname AS lastname_3, t0.email AS email_4, t0.password AS password_5, t0.roles AS roles_6, t0.accreditation AS accreditation_7, t0.is_active AS is_active_8, t0.date_modified AS date_modified_9, t0.last_login AS last_login_10, t0.username AS username_11, t0.date_created AS date_created_12, t0.ID_UTILISATEUR_IDENTITE AS ID_UTILISATEUR_IDENTITE_13, t0.ID_UTILISATEUR_GROUPE AS ID_UTILISATEUR_GROUPE_14 FROM USERS t0 WHERE t0.email = ? LIMIT 1 ["yvon.huynh"] []
[2019-09-16 10:32:05] security.INFO: User for remember-me cookie not found. {"exception":"[object] (Symfony\\Component\\Security\\Core\\Exception\\UsernameNotFoundException(code: 0): User \"yvon.huynh\" not found. at /Users/poste5hookipa/sites/geothermeV2/vendor/symfony/doctrine-bridge/Security/User/EntityUserProvider.php:61)"} []
[2019-09-16 10:32:05] security.DEBUG: Clearing remember-me cookie. {"name":"REMEMBERME"} []
[2019-09-16 10:33:44] security.INFO: Populated the TokenStorage with an anonymous Token. [] []
[2019-09-16 10:34:33] security.DEBUG: Access denied, the user is not fully authenticated; redirecting to authentication entry point. {"exception":"[object] (Symfony\\Component\\Security\\Core\\Exception\\AccessDeniedException(code: 403): Access Denied. at /Users/poste5hookipa/sites/geothermeV2/vendor/symfony/security-http/Firewall/AccessListener.php:72)"} []
[2019-09-16 10:34:33] security.DEBUG: Calling Authentication entry point. [] []
[2019-09-16 10:35:03] request.INFO: Matched route "app_login". {"route":"app_login","route_parameters":{"_route":"app_login","_controller":"App\\Controller\\SecurityController::login"},"request_uri":"http://geothermev2/login","method":"GET"} []
[2019-09-16 10:35:03] security.DEBUG: Checking for guard authentication credentials. {"firewall_key":"main","authenticators":1} []
[2019-09-16 10:35:04] security.DEBUG: Checking support on guard authenticator. {"firewall_key":"main","authenticator":"App\\Security\\LoginFormAuthenticator"} []
[2019-09-16 10:35:04] security.DEBUG: Guard authenticator does not support the request. {"firewall_key":"main","authenticator":"App\\Security\\LoginFormAuthenticator"} []
In the user provider, I set email as the key for authentication in security.yaml :
providers:
db_user:
entity:
class: App\Entity\Users
property: email
but in the logs I read "username", which may be a problem.
When debugging, the token is null, and the request object passed to the
null === $token = $this->rememberMeServices->autoLogin($request) . is NULL.
I thin I have a problem with the Token which i am really not familiar about.
Hey Benoit L. !
Ah, sorry for not replying soon! Great job debugging! Yes, I think you found the root of the problem! Here's how the remember me system works:
1) When a user logs in with remember me, the remember me system calls $user->getUsername()
. It then takes that value (e.g. yvon.huynh
) and stores it in the remember me token.
2) After the session cookie expires, the remember me system loads that remember me token & reads the string from step (1) that it embedded into it (e.g. yvon.huynh
). It then calls $userProvider->loadUserByUsername()
and passes it that string.
So yes, you can see the problem :). The User::getUsername()
method and UserProvider::loadUserByUsername()
methods need to be working in "harmony". You could totally use an email for both... but they need to be consistent. The fix depends on your system. Do users in your system really have both usernames & emails? Or were you just storing usernames because FOSUserBundle kind of forces you to do this?
Anyways, the easiest solution is to make loadUserByUsername()
support both usernames and passwords. It's a simple step: first query by email, and if you get a result, return it. If you don't, query by username and return that result if there is one.
Cheers!
Hi, I couldn't figure out the loasUserByUsername() function, I don't have FOSUserBundle.
However, in the security.yaml file, I change the part providers by :
providers:
db_user:
entity:
class: App\Entity\Users
property: username
And it solved the problem, and that even I login using my email !
Thanks !
Hey Benoit L. !
Nice work! Yep, that is basically how you configure the loadUserByUsername
function to always use "username" to query for the user (which is used with remember me). The "harmony" here is the key: the remember me system calls $user->getUsername()
to get the value to put into the cookie (it always calls this method, this part is not configurable) and then your user provider uses the "username" property to query for the User.
Glad you got it working! Nice job :)
Cheers!
Hey!!
What's the best way to "listen" to the remember me authentication?? I ask this because I need to increase the "expires_at" token for my API clients. Now I only can do this when the user login through the login form.
Hey Carlos!
Cool question! The event you want to listen to is SecurityEvents::INTERACTIVE_LOGIN - you can see it being dispatched from inside the remember me system right here: https://github.com/symfony/...
However, that event is dispatched whenever you log in... in any way - including Guard authentication and pretty much anything else (https://github.com/symfony/.... So... in some ways... this might be perfect! You could listen to this ONE event and increase the expires_at token. But... in other ways... it's not perfect - because it will be triggered also when your API clients authenticate... which means they would be increasing their *own* expires_at!
So, your listener function will be passed an instance of InteractiveLoginEvent (https://github.com/symfony/... - and you can use its getAuthenticationToken() method to get the "token" for how the user is authenticating. This is a super low-level object, but it should be a bit different based on the different ways of authenticating (form login vs guard vs remember me) - so you should be able to use it to determine what "type" of login is currently happening, and whether or not you should increase the expires_at.
Phew! Let me know if that helps!
Cheers!
Has anything changed from Symfony 4 to Symfony 5 regarding "remember me" authentication, when the call is made through AJAX (xhr)? Because in my application, when I was using Symfony 4, everything was working. Now with Symfony 5, it does not authenticate the user. And I see that the browser keeps sending cookies on request. Thanks.
Hey Carlos!
Sorry for the slow reply!
Has anything changed from Symfony 4 to Symfony 5 regarding "remember me" authentication,
Yes and no. A key change was the new anonymous: lazy
feature under your firewall. But this is not something that was "changed automatically" if you upgrade to Symfony 5: it's simply a new feature that's available in Symfony 4.4. If you start a new project, you'll get anonymous: lazy
by default. Otherwise, anonymous: true
should still work the same, but you can "choose" to change to anonymous: lazy
if you want to.
But, there is a bit more to the story :p. This anonymous: lazy
feature didn't quite work right in 4.4.0 or 5.0.0 - it was fixed in 4.4.1 and 5.0.1. So, if you're on 4.4.0 or 5.0.0 you may be getting bad behavior with this anonymous: lazy feature.
If you ARE on the latest version of Symfony and you are still getting bad behavior with remember me, let me know. It could be something else :).
Cheers!
Hello Ryan, happy new year!!! Actually I found out what was the problem (I was getting an AccessDeniedException)... and it's explained here: https://github.com/sensiola...
The problem was with a behavior's change with @IsGranted.
Thank you again!!
Hey Carlos !
Ah, nice find! I've just commented on the attached PR to push it forward :).
Cheers!
Hi, Ryan! First, thanks for your wonderful tutorials! I really enjoy watching them! However, I have an issue. I am working on the project and "Remember me" doesn't work for me. My *security.yaml file:
main:
lazy: true
provider: app_user_provider
form_login: ~
http_basic: ~
custom_authenticator: App\Security\LoginFormAuthenticator
entry_point: App\Security\LoginFormAuthenticator
logout:
path: logout
remember_me:
secret: '%kernel.secret%'
lifetime: 2592000 # 30 days in seconds
path: /
always_remember_me: true```
I set "always_remember_me" to <b>true</b> be sure, that the problem doesn't come from the form.
Also, my *LoginFormAuthenticator:
use TargetPathTrait;
public const LOGIN_ROUTE = 'login';
public function __construct(
private UrlGeneratorInterface $urlGenerator,
private UserRepository $userRepository
) {
}
public function authenticate(Request $request): PassportInterface
{
$email = $request->request->get('email');
$password = $request->request->get('password');
$csrfToken = $request->request->get('_csrf_token');
$user = $this->userRepository->findOneBy(['email' => $request->get('email')]);
if (!$user) {
// fail authentication with a custom error
throw new CustomUserMessageAuthenticationException('Vartotojas nerastas');
}
$request->getSession()->set(Security::LAST_USERNAME, $email);
return new Passport(
new UserBadge($email, function ($userIdentifier) {
return $this->userRepository->findOneBy(['email' => $userIdentifier]);
}),
new PasswordCredentials($password), [
new CsrfTokenBadge('authenticate', $csrfToken)
]);
}
public function onAuthenticationSuccess(
Request $request,
TokenInterface $token,
string $firewallName
): ?Response {
if ($targetPath = $this->getTargetPath($request->getSession(),
$firewallName)) {
return new RedirectResponse($targetPath);
}
return new RedirectResponse($this->urlGenerator->generate('dashboard'));
}
protected function getLoginUrl(Request $request): string
{
return $this->urlGenerator->generate(self::LOGIN_ROUTE);
}
public function supportsRememberMe(): bool
{
return true;
}```
And my *User.php class:
const ROLE_ADMIN = 'ROLE_ADMIN';
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=100)
* @Assert\NotBlank(message="Prašome įvesti email")
*/
private $email;
/**
* @ORM\Column(type="json")
*/
private $roles = [];
/**
* @ORM\Column(type="string", length=255)
*/
private $password;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
private $firstName;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
private $lastName;
/**
* @ORM\Column(type="boolean")
*/
private $isActive = true;
#[Pure] public function __toString(): string
{
return $this->getEmail();
}
#[Pure] public function __construct(array $roles)
{
$this->roles = $roles;
}
public function getId(): ?int
{
return $this->id;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(string $email): self
{
$this->email = $email;
return $this;
}
public function getFirstName(): ?string
{
return $this->firstName;
}
public function setFirstName(?string $firstName): self
{
$this->firstName = $firstName;
return $this;
}
public function getLastName(): ?string
{
return $this->lastName;
}
public function setLastName(?string $lastName): self
{
$this->lastName = $lastName;
return $this;
}
public function getIsActive(): ?bool
{
return $this->isActive;
}
public function setIsActive(bool $isActive): self
{
$this->isActive = $isActive;
return $this;
}
/**
* A visual identifier that represents this user.
*
* @see UserInterface
*/
public function getUsername(): string
{
return $this->getUserIdentifier();
}
/**
* @see UserInterface
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
public function setRoles(array $roles): self
{
$this->roles = $roles;
return $this;
}
/**
* @see UserInterface
*/
public function getPassword(): string
{
return (string) $this->password;
}
public function setPassword(string $password): self
{
$this->password = $password;
return $this;
}
/**
* @see UserInterface
*/
public function getSalt()
{
// not needed when using the "bcrypt" algorithm in security_2.yaml
}
/**
* @see UserInterface
*/
public function eraseCredentials()
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
}
public function getUserIdentifier(): string
{
return (string) $this->email;
}```
I tried:
- clear browser data
- in *security.yaml file change providers->property to <i>username</i> instead of <i>email</i>
- inside <b>remember_me</b> settings add: <i>secure: true</i> and <i>httponly: false</i>
- in *security.yaml set <i>lazy</i> to <b>false</b>
- in *LoginFormAuthenticator delete method <i>supportsRememberMe()</i>
But, eventually, nothing worked. Every time, when I am monitoring "Cookies" field in browser, I see only PHPSESSID cookie and no REMEMBERME cookie.
I am using php8.0 and Symfony 5.3.
To be honest, I do not know what else to try... I would appreciate if you could help me to solve this problem!
Hey Mr_T!
Sorry for the very slow reply!!! I've been working to prep something for the Symfony World conference this week - almost done now :).
I see you're using the new security system/mode (yay!). That helps. Try adding the RememberMeBadge
to your Passport
in the authenticate()
method. I believe this is what you're missing.
It's... kind of odd... but because that badge says "Hey! I support the remember me system!". That's needed so that the remember me cookie isn't added for other authentication mechanisms that shouldn't support it - like an API login. But having that badge alone isn't enough - it says that your authentication supports remember me cookies. But then the system still looks to see if the user opted in. This can be done by adding the _remember_me checkbox to your login form or via the always_remember_me: true
(or, in 5.3, you can also call $rememberMeBadge->enable() before adding it to your passport - https://github.com/symfony/symfony/blob/414c78bf8b9fdbe93173d33a0ae6d449d3aaa021/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/RememberMeBadge.php#L38 ).
Let me know if that helps! To... maybe give you some more information: in the old authentication system, the supportsRememberMe()
was used to say "I support remember me". In the new system, that method is not used anymore - the RememberMeBadge is used instead :).
Cheers!
When your have set a different property in your UserProvider like this:
app_user_provider:
entity:
class: App\Entity\User
property: email
Than the remember-me function will not work.
The TokenBasedRememberMeServices.php
- processAutoLoginCookie will try to load a User:
try {
$user = $this->getUserProvider($class)->loadUserByUsername($username);
}
And inside the EntityUserProvider.php this will fail:
$repository = $this->getRepository();
if (null !== $this->property) {
$user = $repository->findOneBy([$this->property => $username]);
}
Because the $this->property
is 'email' and symfony saves the 'username' inside the cookie.
Is this a Bug or something intended? Would it not be nice, when symfony would save the value inside the cookie hash the UserProvider needs to load the Username?
I am a bit confused :D
Hey Michael B.!
Hmm, let's see. You did some good digging! Here is the relevant part:
Because the
$this->property
is 'email' and symfony saves the 'username' inside the cookie.
That first part about $this->property
is true. But the second part - "symfony saves the 'username' inside the cookie" is not quite true.
When the remember me cookie is created, it calls $user->getUsername()
and THAT is what goes into the cookie - https://github.com/symfony/symfony/blob/0f92ad4fa82fe43436aadb3e6b81f81ffd5c97aa/src/Symfony/Component/Security/Http/RememberMe/TokenBasedRememberMeServices.php#L74
If the actual property that you're using as your "property" is "email", then you need to make getUsername()
actually return $this->email
. I know, it's confusing... because getUsername()
is a terrible method name. It should really be getStringThatUniquelyIdentifiesThisUserWhichCanBeWhateverStringYouWantInYourApp()
;).
If you DO also have a real $username
property, then you have a choice:
1) You could do some reorganizing and do what I said above - ultimately make getUsername()
return the email property.
OR
2) You could instead, under your user provider, set property: username
. This would mean that, anywhere your user provider is used, the "username" will be used to query for the user instead. What ARE those places? It's basically "remember me", "switch_user" and (if you use them) core authentication mechanisms like form_login, json_login or http_basic (if you created a custom authenticator, then you can choose to use your user provider... or just make a direct query in getUser()).
There is a lot to unpack here... so let me know if I can offer more clarification :). It is a confusing spot in Symfony. That user provider is an useful, but often unclear system in Symfony.
Cheers!
Yes, I completly understand that. This is some quite good support here. And I love your videos Ryan. You are amazing.
It is really confusing, why the remember-me-cookie just using the "username"-value and not allows to configure the stored value. It is FORBIDDEN :-D
Haha, I guess... nobody has ever asked before! The built in system "handles everything for you" - putting the username in the cookie... then loading via the username later. You CAN take control of this *entire* system (there is even a "persistent token storage" you can implement... which will be even easier in Symfony 5.3 if you use the new authenticator system - https://symfony.com/doc/cur... )
Cheers!
:) Hi, i am using new authentication system in Sf 5.3 and i followed documentation
https://symfony.com/doc/cur...
and still have same issue that i haven't Remember me in cookies... my property in security.yaml is also email.. i tried as last hope the always_remember_me: true but without any success in cookies :( also cleared log + cache in console
Maybe is it the bug?
Hey Miky!
It's just a guess - but check out my reply over here - https://symfonycasts.com/sc... - you may be missing the RememeberMeBadge - that's not mentioned in the docs. So, that's a docs bug. Well, it's more complicated than that :p. The RememeberMeBadge only applies to the new system - so those docs are still written for the old system and need to be updated to mention this detail.
Let me know if it helps!
Cheers!
this didn't work for me untill i found out that adding a value property to the checkbox was the problem
this works
<input type="checkbox" name="_remember_me"> Remember me
this doesn't
<input type="checkbox" value="remember-me" name="_remember_me"> Remember me
Hey snopi dobi !
Ah, yes! If you don't add a value="" to a checkbox, it defaults to value="on". And this is where that value is checked - notice the "on": https://github.com/symfony/...
Anyways, I'm glad you got it figured out - sorry you got bit by it! We do take the value="" off in the video, but I didn't emphasize that this is *really* important.
Cheers!
I have started a new freshly clean app sf 5 ... and nothing happens. I'm not sure but this feature in my apps doesn't work.
What value should the _remember_me field send when it is checked: "on", "1", "true"....?
🤔🤔🤔🤔🤔🤔🤔🤔
Hey Abelardo L.
Ha! I'm not sure, I believe it's 1
but I think you should not worry about it because the form itself should handle it. If the remember me feature is not working can you double check your config in your config/packages/security.yaml
file? Also double check that the input field name is correct
If everything is right, the last thing to double check is that the method supportsRememberMe()
from your LoginAuthenticator returns true
Cheers!
Great tutorial. I have enjoyed many of them so far.
I believe that I am having an issue with the "remember_me" because I am running Symfony 5.0.7. I followed your suggestion down below to do a tail and the following was the result -
request.ERROR: Uncaught PHP Exception Symfony\Component\HttpKernel\Exception\NotFoundHttpException: "No route found for "GET /m" (from "https://localhost:8000/log-in")" at /Applications/MAMP/htdocs/gc-backend/vendor/symfony/http-kernel/EventListener/RouterListener.php line 136 {"exception":"[object] (Symfony\\Component\\HttpKernel\\Exception\\NotFoundHttpException(code: 0): No route found for \"GET /m\" (from \"https://localhost:8000/log-in\") at /Applications/MAMP/htdocs/gc-backend/vendor/symfony/http-kernel/EventListener/RouterListener.php:136)\n[previous exception] [object] (Symfony\\Component\\Routing\\Exception\\ResourceNotFoundException(code: 0): No routes found for \"/m/\". at /Applications/MAMP/htdocs/gc-backend/vendor/symfony/routing/Matcher/Dumper/CompiledUrlMatcherTrait.php:70)"} []
[2020-05-09T03:14:31.206260+02:00] request.INFO: Matched route "_wdt". {"route":"_wdt","route_parameters":{"_route":"_wdt","_controller":"web_profiler.controller.profiler::toolbarAction","token":"fd6c51"},"request_uri":"https://localhost:8000/_wdt/fd6c51","method":"GET"} []
It cannot find a route "/m" Were we supposed to create this route using annotations?
Hey Tarik M.!
Apologies for my slow reply :). Let me ask some extra questions!
1) About the remember_me, what issue were you experiencing that caused you to start debugging? Was login working but remember_me seems to not work?
2) About the logs, they are indeed very strange! It looks to me like, after you login successfully, you are being redirected to /m
, which is a a very strange URL! I'm not sure where that's coming from. Do you see this in your browser? Do you login and immediately go to /m
and see a 404 page? There are various reasons why you might be redirected to this page, but this is actually within your control. The onAuthenticationSuccess
method - https://symfonycasts.com/screencast/symfony-security/success-user-provider#redirecting-on-success - is responsible for redirecting the user after success. If you are (for some reason) being redirected to /m, it is this method that's doing it.
Let me know if this helps - and some more information about remember_me, and we'll figure out what the issue is :).
Cheers!
Hey @weaverryan, thanks for getting back to me.
1) I noticed that cookie for remember_me was not showing up, like the PHPSESSID was displaying - that's what starting me debugging.
2) I thought the "/m" route was strange as well. That turned out to be a "red herring" so to say - for some reason on the home link, I had an href="m" - I have no idea how that happened, but since I was able to login, yet still no cookie, that led me to dive in.
In the end, I have discovered that it is a Firefox 59.0b version issue, rather than anything in the code, because everything works as expected in Chrome. I responded to another user, where I showed screen shots.
With that said, I had to make a few changes to my security.yaml file for the cookie to show up in Chrome, which I outline in my other response.
One of the things that I noted in that other response, was that I am using Symfony server 4.14.4, which broke a few things from 4.14.3, which I again outline in my other response.
Another thing, was that I am using an older version of Firefox because the project that I am currently working on is for an older audience, hence the need for things to work on older browsers. An older audience has discretionary income and the people running the project, know that it is a must for the project to be functioning for them, more than for the newer browsers, which are used by a younger, less affluent demographic.
I have yet to figure out why Firefox does not store the cookie, but it also does not recognize {{ app.user.username }} - as a result, I have to use the following {{ (app.user)? app.user.username : '' }} at the top of the dashboard area once they are logged in.
It is a bigger issue than cookies, if you cannot pass the User object after login. I have been trying to hunt down if there is some kind of setting for cookies, but if that were the issue, then why do I see has_js cookie and the PHPSESSID cookie? - this is just for my old version of Firefox.
If you have any thoughts on the subject, then please feel free to share. Can you see my other response where I share what I have done so far?
Hey Tarik M.!
Excellent information and digging! Let me do my best to reply to various things :)
With that said, I had to make a few changes to my security.yaml file for the cookie to show up in Chrome, which I outline in my other response
To make sure I'm looking at the right spot, the code you needed was this, correct?
remember_me:
secret: '%kernel.secret%'
lifetime: 2592000 # 30 days in seconds
path: /
always_remember_me: true
If so, this is interesting. I've checked Symfony's core, the only difference compared to the default settings is the lifetime
and always_remember_me
(the path
defaults to /
). The default lifetime is 31536000 (365 days). If this is the setting that truly fixed things, I'm quite surprised, though... it's maybe possible that older Firefox versions rejected cookies that were set for too long of a period. The always_remember_me
makes a bit more sense: with this enabled, you do not need to have the "Remember me" checkbox.
One of the things that I noted in that other response, was that I am using Symfony server 4.14.4, which broke a few things from 4.14.3, which I again outline in my other response
This is a bit puzzling. It's definitely possible that the Symfony binary could have introduced a bug. Based on your other comment - https://symfonycasts.com/screencast/symfony-security/remember-me#comment-4910197050 - it kind of looks like the []
was missing from the POSTed request content? That's very possible, but would be really bizarre - I'm not sure what to think about that.
I have yet to figure out why Firefox does not store the cookie
So, even with the above config, the remember me cookie is not being set using the older version of Firefox? I noticed in your screenshots that, in Firefox, he ssl certificate looks invalid. That could be causing the problem (and the ssl certificate looks ok in chrome). To test this, I would run the symfony binary like this: symfony serve --allow-http
. Then go to the site using http:// not https://. See if that makes a difference.
but it also does not recognize {{ app.user.username }} - as a result, I have to use the following {{ (app.user)? app.user.username : '' }} at the top of the dashboard area once they are logged in.
I can offer some information (maybe?) about this :). I can tell you with 100% certainly that this has nothing to do with your browser :). This is Twig code, so it's evaluated on your server before the browser ever sees it. It is true that you should use something like app.user ? app.user.username : ''
. The reason is that, if you ever run this code in a page where the user is anonymous, app.user
is null, so app.user.username
will break. I do think there is some issue in older Firefox with your remember me cookie, but it looks like everything else about authentication is working fine.
Cheers!
Hi there,
Could you show us the content of these files, please? Routes, twig template and logic.
Thanks in advance. Regards.
First, thank you for getting back to me.
Second, sorry this is a project for work and I have to limit what I share, but I can tell you, that the error began showing up when I changed the following in my Authenticator class - after changing return false to return true:
public function supportsRememberMe()
{
return true;
}
I commented out the return true, but since within the React login form I have the following, which causes the error to return once again:
'_remember_me' : this.state._remember_me - this is posted in the body of the JSON POST when the form is submitted for login
<div classname="checkbox mb-3 login-remember-me">
<label>
<input type="checkbox" id="remember_me" name="_remember_me" defaultchecked="" onchange="{(event)" ==""> this.setState({_remember_me:event.currentTarget.value})}
/> Remember me
</label>
</div>
It seems like Symfony is looking for a route '/m' for the "_remember_me" functionality to function, because as soon as I submit the React form, then I see the following in the logs:
May 11 00:46:20 |WARN | SERVER GET (404) /m
May 11 09:46:20 |ERROR| REQUES Uncaught PHP Exception Symfony\Component\HttpKernel\Exception\NotFoundHttpException: "No route found for "GET /m" (from "https://localhost:8000/log-in")" at /Applications/MAMP/htdocs/gc-backend/vendor/symfony/http-kernel/EventListener/RouterListener.php line 136
I tried so many things today, that I really have no idea if this is what made it work or not, but here is what I have:
public function supportsRememberMe()
{
return true;
}
If this is set to true, then onAuthenticationSuccess expects a request object, so I sent it one:
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
return new Response($request->getUser());
}
I now have a rememberme cookie
My security.yaml is set to:
remember_me:
secret: '%kernel.secret%'
lifetime: 2592000 # 30 days in seconds
path: /
And I updated my symfony serve to 4.14.4 from 4.14.3 which broke many things including having to change my getCredentials function, so I had to re-write it:
public function getCredentials(Request $request)
{
// return $request->headers->get('X-AUTH-TOKEN');
$gCreds = json_decode('[' . $request->getContent() . ']');
$gEmail = $gCreds[0]->email;
$gPswd = $gCreds[0]->password;
$gUsername = $gCreds[0]->username;
$gRememberMe = $gCreds[0]->_remember_me;
$credentials = [
'username' => $gUsername,
'email' => $gEmail,
'password' => $gPswd,
'_remember_me' => $gRememberMe
];
// dd($credentials);
$request->getSession()->set(
Security::LAST_USERNAME,
$credentials['email']
);
return $credentials;
}
I jumped the gun<a href="http://kronusproductions.com/uploads/new_friends/cookies.png"> Screen shot of cookies under the cookies tab</a> but I do not see it under local storage for Firefox, but I did see it <a href="http://kronusproductions.com/uploads/new_friends/cookies2.png">under storage section for Chrome</a>
It maybe because I have a very old version of Firefox and I do not update it for my own reasons, but it is working in Chrome.
As you can see by the screen shots, I am still getting a console error - still haven't figured that one out yet.
I also added another line to the security.yaml file under the remember_me section:
always_remember_me: true
This last step changed the value from being "deleted" to what you see now in the screenshot.
Here's what the logs show:
May 11 20:59:21 |INFO | SERVER POST (200) /login
May 12 05:59:21 |INFO | SECURI Guard authentication successful! authenticator="App\\Security\\TokenAuthenticator" token={"Symfony\\Component\\Security\\Guard\\Token\\PostAuthenticationGuardToken":"PostAuthenticationGuardToken(user=\"someEmail@gmail.com\", authenticated=true, roles=\"ROLE_ADMIN, ROLE_USER\")"}
May 12 05:59:21 |DEBUG| SECURI Guard authenticator set success response. response={"Symfony\\Component\\HttpFoundation\\Response":"HTTP/1.0 200 OK\r\nCache-Control: no-cache, private\r\nDate: Tue, 12 May 2020 03:59:21 GMT\r\n\r\n"}
May 12 05:59:21 |DEBUG| SECURI Clearing remember-me cookie. name="REMEMBERME"
May 12 05:59:21 |DEBUG| SECURI Remember-me was requested; setting cookie.
May 12 05:59:21 |DEBUG| SECURI The "App\Security\TokenAuthenticator" authenticator set the response. Any later authenticator will not be called authenticator="App\\Security\\TokenAuthenticator"
May 12 05:59:21 |DEBUG| SECURI Stored the security token in the session. key="_security_main"
Turns out most of my issues come from the fact that I am using an older version of Firefox. I have the ability to run multiple versions for legacy testing. In the latest version of Firefox, it is functioning as expected see today's screen shot
Hey tam2000k2,
Hm, I wonder how old your Firefox was? Cookie is something that exist really long, so it had to be working I suppose, probably some known bug in that Firefox version, I don't know :/ Thanks for sharing that the version of Firefox might be the problem! Btw, we use Chrome in tests, lately it works better than Firefox.
Cheers!
Hey!
I followed this tutorial and made remember me working fine. Then I've switched to json_login (I want to make a Vue.js app) by following your Api Platform Security tutorial, but I can't make it work with remember me (I get following error : "You must configure at least one remember-me aware listener (such as form-login) for each firewall that has remember-me enabled."). How could I managed it? Should I create my own Authenticator to be able to use it? If so, what is the official Symfony json login Authenticator, from which I could inspire myself for creating my own?
Thanks!
Hi Chloé!
Sorry for my slow reply! Yes, remember me is implemented weird inside of Symfony (something that a core Symfony team member and I [mostly him] are trying to fix right now). Internally, each "authentication mechanism" must "advertise" whether or not they (sort of) "want" to work with remember me. If they don't - json_login does not - then, as you've seen, it won't work with remember me :/. Even if you added a form_login
to your firewall, the error would get away, but the json_login system wouldn't "notify" the remember me system on login, and so the remember me cookie wouldn't get set.
So, unfortunately, I think right now you would need to create your own authenticator for this. And I like your idea of using the core json_login authenticator as inspiration :). But... internally, core "authentication mechanisms" aren't currently implemented as authenticators (that's actually the core of what we're trying to change in security right now - for this exact situation). So, I can point you to a few files, but you'll need to adapt them to an authenticator.
The json_login mechanism is basically these 2 files:
1) https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Security/Http/Firewall/UsernamePasswordJsonAuthenticationListener.php - this kinda does the job of getCredentials
and onAuthenticationSuccess/Failure
for authenticators.
2) https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Security/Core/Authentication/Provider/DaoAuthenticationProvider.php - when the listener calls $this->authenticationManager->authenticate($token);
it is effectively calling that method on THIS class. Though, the actual authenticate()
method is on its PARENT class - called UserAuthenticationProvider
. You'll need to look at both classes to see the flow. But this file is less important - it basically is exchanging the "username" and "password" for the "User" object - e.g. the getUser()
and checkCredentials
methods of an authenticator.
I hope this helps! Let me know how it goes!
Cheers!
Hi weaverryan !
Thanks a lot for your answer and no worries, I'm not super fast either and it's a side-project so there's no urgency. :) I've quickly looked at those files and I can see how I will manage to transform them to an Authenticator. That's great if you're working on enabling Remember me for json_login !
It helps a lot. I don't known when I take time to try and make my custom Authenticator, but I'll surely tell you how it goes.
Cheers!
Thanks for the reply Chloé! If you *do* hit any issues, let us know. But hopefully it will be smooth!
Cheers!
Hi weaverryan
I wanted to thank you again for taking the time to answer me. I've succeeded in making my own "json login authenticator". Actually I mainly used this tutorial, and changed the return statement sto return JsonResponse instead of Response or RedirectResponse). On the front-end, I login by posting a FormData.
My Authenticator (if you want to take a look: https://github.com/chloebrq... ) is, in the end, very simple, and doesn't include all the verifications that are in the 2 files you gave me.
I hope that all is secure enough, and that I did it correctly. (At least, it works perfectly.)
What I'm wondering is, do I need Csrf verification? When I was using json_login I didn't need to thanks to the SameSite attribute, but now I'm using a FormData (just as if I was submitting an html form, I guess?) and I don't know if I'm still "protected" by the SameSite attribute... Things are kinda mixed up in my mind about this. :D
Hi again Chloé!
> I've succeeded in making my own "json login authenticator".
Well done! 🌟I just checked the authenticator code and it is *beautifully* simple and clean. Nice work! I don't see any security problems - you are cleanly plugging into the security system.
> What I'm wondering is, do I need Csrf verification? I don't know if I'm still "protected" by the SameSite attribute... Things are kinda mixed up in my mind about this. :D
Ugh, this stuff is still *so* confusing :). If you're relying on the SameSite attribute (and by "relying on" I simply mean that this IS a valid solution to CSRF attacks, but some small percentage of people using older browsers may not get that protection), then you are STILL protected. You can actually check - after logging in, you will have a session cookie in your browser. If you look at its properties, you should see that it is a SameSite cookie. The "SameSite" cookie feature is actually *not* tied at all to security - it's a property of your "session" storage (your security user is simply stored in the session). In Symfony, whenever you store *anything* in the session (no matter what it is, or, in the case of security, how your authenticator looks), it will be stored with a SameSite cookie.
Cheers!
Hi weaverryan !
Thank you a lot for having checked my authenticator :) I'm really glad I did this right.
And many thanks for your explanation. I'm indeed relying on the SameSite attribute, so now knowing what you've explained, I'm gonna stick with the "SameSite protection" only. That should be enough for my small side-project. :)
I'm grateful you took the time to answer my several questions! Cheers!
hello,
I had an issue of not getting REMEMBERME cookie. As one of the posters suggested you have to uncomment and return true on "supportsRememberMe()" func.
cheers.
// composer.json
{
"require": {
"php": "^7.1.3",
"ext-iconv": "*",
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
"knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
"knplabs/knp-time-bundle": "^1.8", // 1.8.0
"nexylan/slack-bundle": "^2.0,<2.2.0", // v2.0.0
"php-http/guzzle6-adapter": "^1.1", // v1.1.1
"sensio/framework-extra-bundle": "^5.1", // v5.2.0
"stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
"symfony/asset": "^4.0", // v4.1.4
"symfony/console": "^4.0", // v4.1.4
"symfony/flex": "^1.0", // v1.17.6
"symfony/framework-bundle": "^4.0", // v4.1.4
"symfony/lts": "^4@dev", // dev-master
"symfony/orm-pack": "^1.0", // v1.0.6
"symfony/security-bundle": "^4.0", // v4.1.4
"symfony/serializer-pack": "^1.0", // v1.0.1
"symfony/twig-bundle": "^4.0", // v4.1.4
"symfony/web-server-bundle": "^4.0", // v4.1.4
"symfony/yaml": "^4.0", // v4.1.4
"twig/extensions": "^1.5" // v1.5.2
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.0", // 3.0.2
"easycorp/easy-log-handler": "^1.0.2", // v1.0.7
"fzaninotto/faker": "^1.7", // v1.8.0
"symfony/debug-bundle": "^3.3|^4.0", // v4.1.4
"symfony/dotenv": "^4.0", // v4.1.4
"symfony/maker-bundle": "^1.0", // v1.7.0
"symfony/monolog-bundle": "^3.0", // v3.3.0
"symfony/phpunit-bridge": "^3.3|^4.0", // v4.1.4
"symfony/profiler-pack": "^1.0", // v1.0.3
"symfony/var-dumper": "^3.3|^4.0" // v4.1.4
}
}
Hello, when I refresh after deleting the PHPSESSIONID the cookie disappears! is there an order in the security.yaml file?