Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Ralentización de inicio de sesión y eventos

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

El sistema de seguridad de Symfony viene repleto de cosas interesantes, como recordar mi nombre, suplantación de identidad y votantes. Incluso tiene soporte incorporado para un autentificador de "enlace de inicio de sesión", también conocido como "enlaces mágicos de inicio de sesión". En este caso, envías un enlace por correo electrónico a tu usuario y éste hace clic en él para iniciar la sesión.

Otra función muy interesante es el estrangulamiento del inicio de sesión: una forma de evitar que alguien de una única dirección IP pruebe las contraseñas una y otra vez en tu sitio... intentando iniciar sesión una y otra vez. Y es súper fácil de usar.

Activar login_throttling

En tu cortafuegos, habilítalo con login_throttling: true:

security:
... lines 2 - 20
firewalls:
... lines 22 - 24
main:
... lines 26 - 28
login_throttling: true
... lines 30 - 62

Si te detienes ahí mismo... y actualizas cualquier página, obtendrás un error:

El estrangulamiento de inicio de sesión requiere el componente Rate Limiter.

¡Y luego un útil comando para instalarlo! ¡Muy bien! Cópialo, gira a tu terminal y ejecútalo:

composer require symfony/rate-limiter

Este paquete también instala un paquete llamado symfony/lock, que tiene una receta. Ejecuta

git status

para ver lo que ha hecho. Es interesante. Creó un nuevo config/packages/lock.yaml, y también modificó nuestro archivo .env.

Para hacer un seguimiento de los intentos de acceso, el sistema de estrangulamiento necesita almacenar esos datos en algún lugar. Para ello utiliza el componente symfony/lock. Dentro de nuestro archivo.env, en la parte inferior, hay una nueva variable de entorno LOCK_DSN que se establece en semaphore:

34 lines .env
... lines 1 - 28
###> symfony/lock ###
# Choose one of the stores below
# postgresql+advisory://db_user:db_password@localhost/db_name
LOCK_DSN=semaphore
###

Un semáforo... es básicamente una forma súper sencilla de almacenar estos datos si sólo tienes un único servidor. Si necesitas algo más avanzado, consulta la documentación desymfony/lock: muestra todas las diferentes opciones de almacenamiento con sus pros y sus contras. Pero esto nos vendrá muy bien.

Así pues, el paso 1 fue añadir la configuración de login_throttling. El paso 2 fue instalar el componente Rate Limiter. Y el paso 3 es... ¡disfrutar de la función! Sí, ¡hemos terminado!

Refrescar. No hay más errores. Por defecto, esto sólo permitirá 5 intentos de acceso consecutivos para el mismo correo electrónico y dirección IP por minuto. Vamos a probarlo. Uno, dos, tres, cuatro, cinco y... ¡el sexto es rechazado! Nos bloquea durante 1 minuto. Tanto el máximo de intentos como el intervalo se pueden configurar. De hecho, podemos verlo.

En tu terminal, ejecuta:

symfony console debug:config security

Y... busca login_throttling. Ahí está. Sí, este max_attempts está predeterminado a 5 y interval a 1 minuto. Ah, y por cierto, esto también bloqueará que la misma dirección IP haga 5 veces el max_attempts para cualquier correo electrónico. En otras palabras, si la misma dirección IP intentara rápidamente 25 correos electrónicos diferentes, los seguiría bloqueando. Y si quieres una primera línea de defensa impresionante, también te recomiendo encarecidamente que utilices algo como Cloudflare, que puede bloquear a los malos usuarios incluso antes de que lleguen a tu servidor... o activar las defensas si tu sitio es atacado desde muchas direcciones IP.

Profundizando en Cómo Funciona la Ralentización de inicio de sesión

Así que... creo que esta función es bastante genial. Pero lo más interesante para nosotros es cómo funciona entre bastidores. Funciona a través del sistema de oyentes de Symfony. Después de iniciar la sesión, ya sea con éxito o sin éxito, se envían una serie de eventos a lo largo de ese proceso. Podemos engancharnos a esos eventos para hacer todo tipo de cosas interesantes.

Por ejemplo, la clase que contiene la lógica de la aceleración del inicio de sesión se llamaLoginThrottlingListener. Vamos a... ¡abrirla! Pulsa Shift+Shift y abreLoginThrottlingListener.php.

Espectacular. Los detalles dentro de esto no son demasiado importantes. Puedes ver que utiliza algo llamado limitador de velocidad... que se encarga de comprobar si se ha alcanzado el límite. En última instancia, si se ha alcanzado el límite, lanza esta excepción, que provoca el mensaje que hemos visto. Para los que estén atentos, esa excepción se extiende a AuthenticationException... y recuerda que puedes lanzar unAuthenticationException en cualquier punto del proceso de autenticación para que falle.

En cualquier caso, este método está escuchando un evento llamado CheckPassportEvent. Éste se envía después de que se llame al método authenticate() desde cualquier autentificador. En este punto, la autentificación aún no ha tenido éxito... y el trabajo de la mayoría de los oyentes de CheckPassportEvent es hacer alguna comprobación extra y fallar la autentificación si algo ha ido mal.

Esta clase también escucha otro evento llamado LoginSuccessEvent... que... bueno, es bastante obvio: se envía después de cualquier autenticación con éxito. Esto restablece el limitador de velocidad en caso de éxito.

Así que esto está muy bien, y es nuestra primera visión de cómo funciona el sistema de eventos. A continuación, vamos a profundizar descubriendo que casi todas las partes de la autenticación las realiza un oyente. Entonces, crearemos el nuestro.

Leave a comment!

9
Login or Register to join the conversation
Oliver-W Avatar
Oliver-W Avatar Oliver-W | posted hace 1 año

Hi Ryan,
I am sorry, but I can NOT confirm this: "Oh, and by the way, this will also block the same IP address from making 5 times the max_attempts for any email."
My attempts with different email addresses and different passwords where not limited!? Different passwords for the same email, yes. But not if both are different.
I also tested five attempts for the same email with different passwords and then another email. And after six attempts and beeing blocked I can immediately restart with a new email.
Any idea why?
Thx for your good work - your means you AND the team ;-)
Oliver

1 Reply

Hey Oliver W.!

Hmm. So the way it should work is this (assuming max_attempts = 5, which is the default):

A) Only allow 5 attempts per email (and we saw this in the screencast).
B) Only allow max_attempts * 5 (so 5x5=25) attempts (regardless of email) from the same IP address.

I admit that I've never tried pat (B) - I know it from reading the code iirc. But what it means that after you attempt 25 login attempts, it should not allow your IP address to make more attempts (until you reach the cool-down period). Have you tried 26 straight logins quickly (obviously you'll need to change email addresses every 5 attempts to avoid hitting the smaller, email-specific limit).

Let me know if you try that. If I'm wrong, I would definitely like to know!

Cheers!

Reply
Oliver-W Avatar

Sorry Ryan, but I've made so many changes in the meantime that I don't think my tests would be of any value 8-(.
If I find any occasion to do it, I will.

Reply

So... if I get blocked for too many attempts, and then login successfully (say I created a fake user just for this), I'm not blocked anymore from attempting logins with a different e-mail than my fake user's? I gotta try this... (Rubs hands together) 😏

Reply

Hey @D-Marti

That's a great question, honestly, I don't know the inner details of how the login throttling system works, but I guess it blocks you by IP, not only by email. If you give this a try please let me know your findings :)

Cheers!

Reply

Indeed. Once you get blocked, you can only login if you provide valid credentials. Once you do that, and logout, even if you clear all your cookies, and try loging in with a different user and a bad password, your IP will still be blocked.

TL;DR The block timer does not get reset with a successful login. Blocking is IP based.

Which is great! 😲

Reply

Great research! Thank you for sharing it ✌

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

Hey Ryan. It would be nice to see how you can use something like cloudfare or fastly to block bad users or use it as a reverse proxy, caching, esi and so on. Can you do such a tutorial some time pleeeeasse?

Reply

Hey Michael,

Thank you for your interest in SymfonyCasts tutorials and sharing this idea with us! Unfortunately, no certain plans to do this in the near future, but we will consider releasing a course about it later... or probably cover it at least partially in future courses maybe. We'll add this to our idea pool for now, but I can't say when it might be released yet.

About ESI, we kinda covered it before in previous tutorials, I'd recommend you to take a look at it: https://symfonycasts.com/se... - the code is based on an older version of Symfony, but the concepts it covers are still valid for today. And also, here we were talking about reverse proxy: https://symfonycasts.com/sc... and caching: https://symfonycasts.com/sc... - probably it will be useful for you too.

Feel free to play with our search to find more related topics, it's really powerful and searches in video, code blocks, comments, etc :)

Cheers!

1 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": ">=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
    }
}
userVoice