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 SubscribeAhora que tenemos el sistema "recuérdame" funcionando, ¡juguemos con él! En lugar de dar al usuario la opción de activar "recuérdame", ¿podríamos... activarlo siempre?
En este caso, ya no necesitamos la casilla "Recuérdame", así que la eliminamos por completo.
Hay dos formas de "forzar" al sistema remember me a establecer siempre una cookie aunque no esté la casilla de verificación. La primera es en security.yaml
: establecer always_remember_me:
en true
:
security: | |
... lines 2 - 16 | |
firewalls: | |
... lines 18 - 20 | |
main: | |
... lines 22 - 27 | |
remember_me: | |
... line 29 | |
always_remember_me: true | |
... lines 31 - 43 |
Sí, acabo de escribir mal remember
... ¡así que no lo hagas!
Con esto, nuestro autentificador sigue necesitando añadir un RememberMeBadge
:
... lines 1 - 23 | |
class LoginFormAuthenticator extends AbstractAuthenticator | |
{ | |
... lines 26 - 39 | |
public function authenticate(Request $request): PassportInterface | |
{ | |
... lines 42 - 44 | |
return new Passport( | |
... lines 46 - 55 | |
new PasswordCredentials($password), | |
[ | |
... lines 58 - 61 | |
new RememberMeBadge(), | |
] | |
); | |
} | |
... lines 66 - 92 | |
} |
Pero el sistema ya no buscará esa casilla. Mientras vea esta insignia, añadirá la cookie.
La otra forma de habilitar la cookie "Recuérdame" en todas las situaciones es a través de la propia insignia. Comenta la nueva opción. Bueno... déjame arreglar mi error tipográfico y luego comentarlo:
security: | |
... lines 2 - 16 | |
firewalls: | |
... lines 18 - 20 | |
main: | |
... lines 22 - 27 | |
remember_me: | |
... line 29 | |
#always_remember_me: true | |
... lines 31 - 43 |
Dentro de LoginFormAuthenticator
, en la propia insignia, puedes llamar a ->enable()
... que devuelve la instancia de la insignia:
... lines 1 - 23 | |
class LoginFormAuthenticator extends AbstractAuthenticator | |
{ | |
... lines 26 - 39 | |
public function authenticate(Request $request): PassportInterface | |
{ | |
... lines 42 - 44 | |
return new Passport( | |
... lines 46 - 55 | |
new PasswordCredentials($password), | |
[ | |
... lines 58 - 61 | |
(new RememberMeBadge())->enable(), | |
] | |
); | |
} | |
... lines 66 - 92 | |
} |
Esto dice:
No me interesa ninguna otra configuración ni la casilla de verificación: Definitivamente quiero que el sistema remember me añada una cookie.
¡Vamos a probarlo! Borra la sesión y la cookie REMEMBERME
. Esta vez, cuando iniciemos la sesión... ¡oh, token CSRF no válido! Eso es porque acabo de matar mi sesión sin refrescar - ¡tonto Ryan! Refresca e inténtalo de nuevo.
¡Muy bien! ¡Tenemos la cookie REMEMBERME
!
Hay una cosa con la que debes tener cuidado cuando se trata de las cookies "Recuérdame". Si un usuario malintencionado consiguiera de algún modo acceder a mi cuenta -por ejemplo, si robara mi contraseña-, podría, por supuesto, iniciar la sesión. Normalmente, eso es un asco... pero en cuanto lo descubra, podría cambiar mi contraseña, lo que les desconectaría.
Pero... si ese mal usuario tiene una cookie de REMEMBERME
... entonces, aunque cambie mi contraseña, seguirá conectado hasta que esa cookie caduque... lo que podría ser dentro de mucho tiempo. Estas cookies son casi tan buenas como las reales: actúan como "billetes de autentificación gratuitos". Y siguen funcionando -independientemente de lo que hagamos- hasta que caducan.
Afortunadamente, en el nuevo sistema de autenticación, hay una forma muy interesante de evitar esto. En security.yaml
, debajo de remember_me
, añade una nueva opción llamadasignature_properties
configurada en un array con password
dentro:
security: | |
... lines 2 - 16 | |
firewalls: | |
... lines 18 - 20 | |
main: | |
... lines 22 - 27 | |
remember_me: | |
... line 29 | |
signature_properties: [password] | |
... lines 31 - 44 |
Me explico. Cuando Symfony crea la cookie remember me, crea una "firma" que demuestra que esta cookie es válida. Gracias a esta configuración, ahora obtendrá la propiedadpassword
de nuestro User
y la incluirá en la firma. Luego, cuando esa cookie se utilice para autenticarse, Symfony volverá a crear la firma utilizando el password
del User
que está actualmente en la base de datos y se asegurará de que las dos firmas coincidan. Así que si el password
de la base de datos es diferente a la contraseña que se utilizó para crear originalmente la cookie... ¡la coincidencia de la firma fallará!
En otras palabras, para cualquier propiedad de esta lista, si incluso una de estas cambia en la base de datos en ese User
, todas las cookies "recuérdame" para ese usuario serán invalidadas instantáneamente.
Así que si un usuario malo me roba la cuenta, todo lo que tengo que hacer es cambiar mi contraseña y ese usuario malo será expulsado.
Esto es superguay verlo en acción. Actualiza la página. Si modificas la configuración designature_properties
, se invalidarán todas las cookies de REMEMBERME
en todo el sistema: así que asegúrate de que la configuración es correcta cuando lo configures por primera vez. Observa: si borro la cookie de sesión y actualizo... ¡sí! No estoy autentificado: la cookie de REMEMBERME
no ha funcionado. Sigue ahí... pero no es funcional.
Iniciemos la sesión - con nuestra dirección de correo electrónico normal... y la contraseña... para que obtengamos una nueva cookie remember me que se crea con la contraseña con hash.
¡Genial! Y ahora, en condiciones normales, las cosas funcionarán como siempre. Puedo borrar la cookie de sesión, actualizarla y seguiré conectado.
Pero ahora, vamos a cambiar la contraseña del usuario en la base de datos. Podemos hacer trampa y hacer esto en la línea de comandos:
symfony console doctrine:query:sql 'UPDATE user SET password="foo" WHERE email = "abraca_admin@example.com"'
Poner la contraseña en foo
es una auténtica tontería... ya que esta columna debe contener una contraseña con hash... pero estará bien para nuestros propósitos. Pulsa y... ¡fantástico! Esto imita lo que ocurriría si cambiara la contraseña de mi cuenta.
Ahora, si somos el usuario malo, la próxima vez que volvamos al sitio... ¡de repente habremos cerrado la sesión! ¡Una barbaridad! ¡Y yo también me habría salido con la mía si no fuera por vosotros, niños entrometidos! La cookie "recuérdame" está ahí... pero no funciona. Me encanta esta función.
Volvamos atrás... y recarguemos nuestras instalaciones para arreglar mi contraseña:
symfony console doctrine:fixtures:load
Y... una vez hecho esto, vuelve a conectarte como abraca_admin@example.com
, contraseña tada
.
A continuación: ¡es hora de tener un viaje de poder y empezar a negar el acceso! Veamosaccess_control
: la forma más sencilla de bloquear el acceso a secciones enteras de tu sitio.
I have to start remembering to do this by default to help our Windows users :).
Thanks for the note!
The signature_property password seems to be default in symfony 5.4, at least it shows up in my debug (list of all settings) without me mentioning it in my config file =)
Hey @MattWelander!
Yes, good catch! The password
was added by default in a security patch - https://symfony.com/blog/cve-2021-41268-remember-me-cookie-persistance-after-password-changes - because it's actually SUPER important for the user to be logged out when the password changes. So now, that part is done for you nicely :).
Cheers!
Hi!
Is it possible to also invalidate all other (without Remember Me) active user sessions when password is changed?
Hey Aigars,
Actually, this is exactly how things should work in Security component. As soon as user's password is changed - it would invalidate and automatically log out the user everywhere. IIRC that's should be possible thanks to serialize/unserialize User method where you include the password, email, etc. fields that are sensitive for the security.
Cheers!
I see
`signature_properties:
- password`
is the default now, it works even if not set in the yaml. But this is useful for other properties such as the status, you don't want them to keep logging in after they are banned.
But is this the same as doing it in the User::isEqualTo() method?
if($this->status !== $user->getStatus()) {
return false;
}
Hey The_nuts,
Nice, good to have it by default :)
Yeah, good question! Well, the purpose of isEqualTo() and signature_properties are a bit different, but they have kinda similar behaviour. Personally, I think I'd probably keep status in isEqualTo(), and if user object "changed" - it would be logged out anyway.
Cheers!
I was curious about remember me's being invalidated on password change with the old auth system. Appears they are - I believe this is because of the refresh user process and the AbstractToken::hasUserChanged()
method. I assume this would be the same with the new authenticator but using signature_properties
, it would "fail earlier" (before authentication). Am I off-base here?
Yo Kevin B.!
Appears they are - I believe this is because of the refresh user process and the AbstractToken::hasUserChanged() method
The old system invalidates the remember me cookies when the password changed? Are you positive about that? It's possible... but it's not what I would have expected. Yes, you're totally correct that the hasUserChanged()
would "fail" when the user's password changes. That should invalidate the User object that's stored in the session... so it should have basically the same effect as deleting the "session" cookie. But then, the "remember me" system would take over: it would read the cookie, grab the "user identifier" from that, query for a refresh user, authenticate them and then store that new user in the session.
So let me know if you're seeing something different - that would be very interesting. I did test the new system for this behavior while I recorded (I changed the user's password BEFORE adding signature_properties and the result was that the user was still logged in, but suddenly via the remember me cookie instead of the original "token"). But, if you get a different result, we should look deeper :).
Cheers!
Figured it out. The old auth system uses <a href="https://github.com/symfony/symfony/blob/5.4/src/Symfony/Component/Security/Http/RememberMe/TokenBasedRememberMeServices.php">TokenBasedRememberMeServices</a> and this hashes the cookie with the user's password (because UserInterface
has the getPassword()
method still). The new auth system uses <a href="SignatureRememberMeHandler">SignatureRememberMeHandler</a> which, because UserInterface
does not have a getPassword()
method anymore, can't* know to create the hash with the password.
*It could if it checked if the user implemented PasswordAuthenticatedUserInterface
though...
Glad I have a test covering this for when I upgrade to the new auth system!
Ah ha! Good digging! So in the old system, there was almost a "hardcoded" signature_properties, which included the password. With the new system, you need can control the signature.
// composer.json
{
"require": {
"php": ">=8.1",
"ext-ctype": "*",
"ext-iconv": "*",
"babdev/pagerfanta-bundle": "^3.3", // v3.3.0
"composer/package-versions-deprecated": "^1.11", // 1.11.99.4
"doctrine/annotations": "^1.0", // 1.13.2
"doctrine/doctrine-bundle": "^2.1", // 2.6.3
"doctrine/doctrine-migrations-bundle": "^3.0", // 3.1.1
"doctrine/orm": "^2.7", // 2.10.1
"knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
"knplabs/knp-time-bundle": "^1.11", // v1.16.1
"pagerfanta/doctrine-orm-adapter": "^3.3", // v3.3.0
"pagerfanta/twig": "^3.3", // v3.3.0
"phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
"scheb/2fa-bundle": "^5.12", // v5.12.1
"scheb/2fa-qr-code": "^5.12", // v5.12.1
"scheb/2fa-totp": "^5.12", // v5.12.1
"sensio/framework-extra-bundle": "^6.0", // v6.2.0
"stof/doctrine-extensions-bundle": "^1.4", // v1.6.0
"symfony/asset": "5.3.*", // v5.3.4
"symfony/console": "5.3.*", // v5.3.7
"symfony/dotenv": "5.3.*", // v5.3.8
"symfony/flex": "^1.3.1", // v1.17.5
"symfony/form": "5.3.*", // v5.3.8
"symfony/framework-bundle": "5.3.*", // v5.3.8
"symfony/monolog-bundle": "^3.0", // v3.7.0
"symfony/property-access": "5.3.*", // v5.3.8
"symfony/property-info": "5.3.*", // v5.3.8
"symfony/rate-limiter": "5.3.*", // v5.3.4
"symfony/runtime": "5.3.*", // v5.3.4
"symfony/security-bundle": "5.3.*", // v5.3.8
"symfony/serializer": "5.3.*", // v5.3.8
"symfony/stopwatch": "5.3.*", // v5.3.4
"symfony/twig-bundle": "5.3.*", // v5.3.4
"symfony/ux-chartjs": "^1.3", // v1.3.0
"symfony/validator": "5.3.*", // v5.3.8
"symfony/webpack-encore-bundle": "^1.7", // v1.12.0
"symfony/yaml": "5.3.*", // v5.3.6
"symfonycasts/verify-email-bundle": "^1.5", // v1.5.0
"twig/extra-bundle": "^2.12|^3.0", // v3.3.3
"twig/string-extra": "^3.3", // v3.3.3
"twig/twig": "^2.12|^3.0" // v3.3.3
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.0
"symfony/debug-bundle": "5.3.*", // v5.3.4
"symfony/maker-bundle": "^1.15", // v1.34.0
"symfony/var-dumper": "5.3.*", // v5.3.8
"symfony/web-profiler-bundle": "5.3.*", // v5.3.8
"zenstruck/foundry": "^1.1" // v1.13.3
}
}
Reminder for Windows users -- if you get the following error you have to switch the single and double quotes around:
"Too many arguments to "doctrine:query:sql" command, expected arguments "sql".
Use this as the command:
symfony console doctrine:query:sql "UPDATE user SET password='foo' WHERE email = 'abraca_admin@example.com'"