Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Recordarme siempre y "signature_properties"

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Ahora 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.

always_remember_me: true

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.

Habilitación en el RememberMeBadge

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!

Asegurar las cookies Remember Me: Invalidar al cambiar los datos del usuario

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 REMEMEBERME... 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.

Leave a comment!

14
Login or Register to join the conversation
MattWelander Avatar
MattWelander Avatar MattWelander | posted hace 3 meses

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 =)

Reply

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!

Reply
Aigars Z. Avatar
Aigars Z. Avatar Aigars Z. | posted hace 8 meses

Hi!
Is it possible to also invalidate all other (without Remember Me) active user sessions when password is changed?

Reply

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!

2 Reply
Aigars Z. Avatar

Thank you for very fast response!
It seems, that I mixed up users when performing tests.

1 Reply

Hey Aigars,

Yeah, most probably so :) Anyway, I'm glad my tips helped.

Cheers!

1 Reply
Trafficmanagertech Avatar
Trafficmanagertech Avatar Trafficmanagertech | posted hace 1 año

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;
}

Reply

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!

Reply
akincer Avatar
akincer Avatar akincer | posted hace 1 año

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'"

Reply

I have to start remembering to do this by default to help our Windows users :).

Thanks for the note!

Reply
Kevin B. Avatar
Kevin B. Avatar Kevin B. | posted hace 1 año

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?

Reply

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!

Reply
Kevin B. Avatar

Figured it out. The old auth system uses TokenBasedRememberMeServices and this hashes the cookie with the user's password (because UserInterface has the getPassword() method still). The new auth system uses SignatureRememberMeHandler 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!

Reply

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.

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

¡Este tutorial también funciona muy bien para Symfony 6!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.4.1 || ^8.0.0",
        "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
    }
}