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 SubscribeOtra buena característica de un formulario de acceso es la casilla "recuérdame". Aquí almacenamos una cookie "recuérdame" de larga duración en el navegador del usuario, de modo que cuando cierre su navegador -y por tanto, pierda su sesión- esa cookie le mantendrá conectado... durante una semana... o un año... o lo que configuremos. Añadamos esto.
El primer paso es ir a config/packages/security.yaml
y activar el sistema. Lo hacemos diciendo remember_me:
y, a continuación, estableciendo una pieza de configuración necesaria: secret
: establecer en %kernel.secret%
:
security: | |
... lines 2 - 16 | |
firewalls: | |
... lines 18 - 20 | |
main: | |
... lines 22 - 27 | |
remember_me: | |
secret: '%kernel.secret%' | |
... lines 30 - 42 |
Esto se utiliza para "firmar" el valor de la cookie remember me... y el parámetro kernel.secret
viene en realidad de nuestro archivo .env
:
... lines 1 - 15 | |
###> symfony/framework-bundle ### | |
... line 17 | |
APP_SECRET=c28f3d37eba278748f3c0427b313e86a | |
### | |
... lines 20 - 28 |
Sí, este APP_SECRET
acaba convirtiéndose en el parámetro kernel.secret
... al que podemos hacer referencia aquí.
Como es normal, hay un montón de otras opciones que puedes poner en remember_me
... y puedes ver muchas de ellas ejecutando:
symfony console debug:config security
Busca la sección remember_me:
. Una importante es lifetime:
, que es el tiempo de validez de la cookie "Recuérdame".
Antes he dicho que la mayor parte de la configuración que ponemos bajo nuestro cortafuegos sirve para activar diferentes autentificadores. Por ejemplo, custom_authenticator:
activa nuestro LoginFormAuthenticator
:
security: | |
... lines 2 - 16 | |
firewalls: | |
... lines 18 - 20 | |
main: | |
... lines 22 - 23 | |
custom_authenticator: App\Security\LoginFormAuthenticator | |
... lines 25 - 42 |
Lo que significa que ahora se llama a nuestra clase al inicio de cada petición y se busca el envío de un formulario de acceso. La configuración de remember_me
también activa un autentificador: un autentificador central llamado RememberMeAuthenticator
. En cada petición, éste busca una cookie "recuérdame" -que crearemos en un segundo- y, si está ahí, la utiliza para autenticar al usuario.
Ahora que esto está en su sitio, nuestro siguiente trabajo es establecer esa cookie en el navegador del usuario después de que se conecte. Abre login.html.twig
. En lugar de añadir siempre la cookie, dejemos que el usuario elija. Justo después de la contraseña, añade un div con algunas clases, una etiqueta y una entrada type="checkbox"
,name="_remember_me"
:
... lines 1 - 4 | |
{% block body %} | |
<div class="container"> | |
<div class="row"> | |
<div class="login-form bg-light mt-4 p-4"> | |
<form method="post" class="row g-3"> | |
... lines 10 - 24 | |
<div class="form-check mb-3"> | |
<label> | |
<input type="checkbox" name="_remember_me" class="form-check-input"> Remember me | |
</label> | |
</div> | |
... lines 30 - 39 | |
</form> | |
</div> | |
</div> | |
</div> | |
{% endblock %} |
El nombre - _remember_me
- es importante y tiene que ser ese valor. Como veremos en un minuto, el sistema busca una casilla de verificación con este nombre exacto.
Bien, actualiza el formulario. Genial, ¡tenemos una casilla de verificación! Aunque... es un poco feo... creo que se ha estropeado algo. Usa form-check
y démosle a nuestra casilla de verificaciónform-check-input
:
... lines 1 - 4 | |
{% block body %} | |
<div class="container"> | |
<div class="row"> | |
<div class="login-form bg-light mt-4 p-4"> | |
<form method="post" class="row g-3"> | |
... lines 10 - 24 | |
<div class="form-check mb-3"> | |
<label> | |
<input type="checkbox" name="_remember_me" class="form-check-input"> Remember me | |
</label> | |
</div> | |
... lines 30 - 39 | |
</form> | |
</div> | |
</div> | |
</div> | |
{% endblock %} |
Ahora... ¡mejor!
Si marcáramos la casilla y la enviáramos... no pasaría absolutamente nada diferente: Symfony no establecería una cookie "Recuérdame".
Esto se debe a que nuestro autentificador necesita anunciar que admite el establecimiento de cookies remember me. Esto es un poco raro, pero piénsalo: el hecho de que hayamos activado el sistema remember_me
en security.yaml
no significa que queramos que SIEMPRE se establezcan cookies remember me. En un formulario de inicio de sesión, definitivamente. Pero si tuviéramos algún tipo de autenticación con token de la API... entonces no querríamos que Symfony intentara establecer una cookie remember me en esa petición de la API.
En cualquier caso, todo lo que tenemos que añadir es una pequeña bandera que diga que este mecanismo de autenticación sí admite añadir cookies remember me. Hazlo con una insignia: new RememberMeBadge()
:
... lines 1 - 16 | |
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge; | |
... lines 18 - 23 | |
class LoginFormAuthenticator extends AbstractAuthenticator | |
{ | |
... lines 26 - 39 | |
public function authenticate(Request $request): PassportInterface | |
{ | |
... lines 42 - 44 | |
return new Passport( | |
new UserBadge($email, function($userIdentifier) { | |
... lines 47 - 55 | |
new PasswordCredentials($password), | |
[ | |
... lines 58 - 61 | |
new RememberMeBadge(), | |
] | |
); | |
} | |
... lines 66 - 92 | |
} |
¡Eso es todo! Pero hay una cosa rara. Con el CsrfTokenBadge
, leemos el token POSTed y se lo pasamos a la insignia. Pero con RememberMeBadge
... no hacemos eso. En su lugar, internamente, el sistema "recuérdame" sabe que debe buscar una casilla llamada, exactamente, _remember_me
.
Todo el proceso funciona así. Después de que nos autentifiquemos con éxito, el sistema "recuérdame" buscará esta insignia y mirará si esta casilla está marcada. Si ambas cosas son ciertas, añadirá la cookie "recuérdame".
Veamos esto en acción. Actualiza la página... e introduce nuestro correo electrónico normal, la contraseña "tada", haz clic en la casilla "Recuérdame"... y pulsa "Iniciar sesión". La autenticación se ha realizado con éxito No es ninguna sorpresa. Pero ahora abre las herramientas de tu navegador, ve a "Aplicación", busca "Cookies" y... ¡sí! Tenemos una nueva cookie REMEMBERME
... que caduca dentro de mucho tiempo: ¡es decir, dentro de 1 año!
Para demostrar que el sistema funciona, elimina la cookie de sesión que normalmente nos mantiene conectados. Observa lo que ocurre cuando actualizamos. ¡Seguimos conectados! Eso es gracias al autentificador remember_me
.
Cuando te autentificas, internamente, tu objeto User
se envuelve en un objeto "token"... que normalmente no es demasiado importante. Pero ese token muestra cómo te has autentificado. Ahora dice RememberMeToken
... lo que demuestra que la cookie "Recuérdame" fue la que nos autenticó.
Ah, y si te preguntas por qué Symfony no ha añadido una nueva cookie de sesión... eso es sólo porque la sesión de Symfony es perezosa. No lo verás hasta que vayas a una página que utilice la sesión - como la página de inicio de sesión. Ahora está de vuelta.
Y... ¡eso es todo! Además de nuestro LoginFormAuthenticator
, ahora hay un segundo autentificador que busca información de autentificación en una cookie deREMEMBERME
.
Sin embargo, podemos hacer todo esto un poco más elegante. A continuación, vamos a ver cómo podríamos añadir una cookie "Recuérdame" para todos los usuarios cuando se conecten, sin necesidad de una casilla de verificación. También vamos a explorar una nueva opción del sistema "recuérdame" que permite invalidar todas las cookies "recuérdame" existentes si el usuario cambia su contraseña.
Hmm... I found the setting, so I set my cookie REMEMBERME to httponly: false
(under remember_me in the security.yaml
), but the behavior was still present, so apparently it wasn't caused by that setting. I kept digging and realized that I was testing the authenticated status with if ($this->isGranted('IS_AUTHENTICATED_FULLY')
which returns false if you are logged in through a cookie. I changed it to if ($this->isGranted('IS_AUTHENTICATED_FULLY') || $this->isGranted('IS_AUTHENTICATED_REMEMBERED'))
and it works like a charm, without lowering the bar by setting httponly: false
, I was out in left field there =)
Hey Matt,
Good catch! I'm glad you were able to figure it out yourself. Yes, that IS_AUTHENTICATED_FULLY
ignores the REMEMBER_ME cookies and looks only at the session - it's useful for some parts of your app where you want have extra security, e.g. the change password page. In this cases, even if the user is still authenticated via REMEMBER_ME - we still want user to log in fully first to be able to change their password.
About that if ($this->isGranted('IS_AUTHENTICATED_FULLY') || $this->isGranted('IS_AUTHENTICATED_REMEMBERED'))
- it's redundant here to check for IS_AUTHENTICATED_FULLY, it's just enough to check for IS_AUTHENTICATED_REMEMBERED and that's it. I.e. you either check for IS_AUTHENTICATED_FULLY or IS_AUTHENTICATED_REMEMBERED, but checking both in the same if clause has no sense :)
Cheers!
// 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
}
}
Hi! Seems like it's been even more simplified with sym 5.4, using the main: form_login authenticator all I needed to do was add the checkbox to the view, name it _remember_me like you explained, and symfony picks it right up.
A question though - how can I set the cookie to httpOnly:false? I make som asynchronous requests (jQuery ajax) for which this cookie is not valid, so parts of my site still works, while other parts (built on ajax requests) tell the user "you are not logged in".