Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Sistema de eventos de seguridad y protección Csrf

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

Después de devolver el objeto Passport, sabemos que ocurren dos cosas. En primer lugar, elUserBadge se utiliza para obtener el objeto User:

... lines 1 - 21
class LoginFormAuthenticator extends AbstractAuthenticator
{
... lines 24 - 37
public function authenticate(Request $request): PassportInterface
{
... lines 40 - 42
return new Passport(
new UserBadge($email, function($userIdentifier) {
// optionally pass a callback to load the User manually
$user = $this->userRepository->findOneBy(['email' => $userIdentifier]);
if (!$user) {
throw new UserNotFoundException();
}
return $user;
}),
... line 54
);
}
... lines 57 - 83
}

En nuestro caso, como le pasamos un segundo argumento, sólo llama a nuestra función, y nosotros hacemos el trabajo. Pero si sólo pasas un argumento, entonces el proveedor del usuario hace el trabajo.

Lo segundo que ocurre es que se "resuelve" la "placa de credenciales":

... lines 1 - 21
class LoginFormAuthenticator extends AbstractAuthenticator
{
... lines 24 - 37
public function authenticate(Request $request): PassportInterface
{
... lines 40 - 42
return new Passport(
... lines 44 - 53
new PasswordCredentials($password)
);
}
... lines 57 - 83
}

Originalmente lo hacía ejecutando nuestra llamada de retorno. Ahora comprueba la contraseña del usuario en la base de datos.

El sistema de eventos en acción

Todo esto está impulsado por un sistema de eventos realmente genial. Después de nuestro método authenticate(), el sistema de seguridad envía varios eventos... y hay un conjunto de oyentes de estos eventos que hacen diferentes trabajos. Más adelante veremos una lista completa de estos oyentes... e incluso añadiremos nuestros propios oyentes al sistema.

UserProviderListener

Pero veamos algunos de ellos. Pulsa Shift+Shift para que podamos cargar algunos archivos del núcleo de Symfony. El primero se llama UserProviderListener. Asegúrate de "Incluir elementos que no sean del proyecto"... y ábrelo.

Se llama después de que devolvamos nuestro Passport. Primero comprueba que elPassport tiene un UserBadge -siempre lo tendrá en cualquier situación normal- y luego coge ese objeto. A continuación, comprueba si la placa tiene un "cargador de usuario": es la función que pasamos al segundo argumento de nuestro UserBadge. Si la placa ya tiene un cargador de usuario, como en nuestro caso, no hace nada. Pero si no lo tiene, establece el cargador de usuarios en el método loadUserByIdentifier() de nuestro proveedor de usuarios.

Es... un poco técnico... pero esto es lo que hace que nuestro proveedor de usuario ensecurity.yaml se encargue de cargar el usuario si sólo pasamos un argumento a UserBadge.

CheckCredentialsListener

Vamos a comprobar otra clase. Cierra ésta y pulsa Shift+Shift para abrirCheckCredentialsListener. Como su nombre indica, se encarga de comprobar las "credenciales" del usuario. Primero comprueba si el Passport tiene una credencialPasswordCredentials. Aunque su nombre no lo parezca, los objetos "credenciales" son sólo insignias... como cualquier otra insignia. Así que esto comprueba si el Passport tiene esa insignia y, si la tiene, coge la insignia, lee la contraseña en texto plano de ella y, finalmente aquí abajo, utiliza el hasher de contraseñas para verificar que la contraseña es correcta. Así que esto contiene toda la lógica del hash de la contraseña. Más abajo, este oyente también se encarga de la insignia CustomCredentials.

Las insignias deben ser resueltas

Así que tu Passport siempre tiene al menos estas dos insignias: la UserBadge y también algún tipo de "insignia de credenciales". Una propiedad importante de las insignias es que cada una debe estar "resuelta". Puedes ver esto en CheckCredentialsListener. Cuando termina de comprobar la contraseña, llama a $badge->markResolved(). Si, por alguna razón, no se llamara a este CheckCredentialsListener debido a alguna configuración errónea... la insignia quedaría "sin resolver" y eso haría que la autenticación fallara. Sí, después de llamar a los listeners, Symfony comprueba que todas las insignias se han resuelto. Esto significa que puedes devolver con confianzaPasswordCredentials y no tener que preguntarte si algo ha verificado realmente esa contraseña.

Añadir protección CSRF

Y aquí es donde las cosas empiezan a ponerse más interesantes. Además de estas dos insignias, podemos añadir más insignias a nuestro Passport para activar más superpoderes. Por ejemplo, una cosa buena para tener en un formulario de inicio de sesión es la protección CSRF. Básicamente, añades un campo oculto a tu formulario que contenga un token CSRF... y luego, al enviar, validas ese token.

Hagamos esto. En cualquier lugar dentro de tu formulario, añade una entrada type="hidden",name="_csrf_token" - este nombre podría ser cualquier cosa, pero es un nombre estándar - y luego value="{{ csrf_token() }}". Pásale la cadena authenticate:

... 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
<input type="hidden" name="_csrf_token"
value="{{ csrf_token('authenticate') }}"
>
... lines 28 - 33
</form>
</div>
</div>
</div>
{% endblock %}

Ese authenticate también podría ser cualquier cosa... es como un nombre único para este formulario.

Ahora que tenemos el campo, copia su nombre y dirígete a LoginFormAuthenticator. Aquí, tenemos que leer ese campo de los datos POST y luego preguntar a Symfony:

¿Es válido este token CSRF?

Bueno, en realidad, esa segunda parte ocurrirá automáticamente.

¿Cómo? El objeto Passport tiene un tercer argumento: un array de otras fichas que queramos añadir. Añade una: una nueva CsrfTokenBadge():

... lines 1 - 15
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
... lines 17 - 22
class LoginFormAuthenticator extends AbstractAuthenticator
{
... lines 25 - 38
public function authenticate(Request $request): PassportInterface
{
... lines 41 - 43
return new Passport(
... lines 45 - 55
[
new CsrfTokenBadge(
... lines 58 - 59
)
]
);
}
... lines 64 - 90
}

Esto necesita dos cosas. La primera es el identificador del token CSRF. Digamos authenticate:

... lines 1 - 22
class LoginFormAuthenticator extends AbstractAuthenticator
{
... lines 25 - 38
public function authenticate(Request $request): PassportInterface
{
... lines 41 - 43
return new Passport(
... lines 45 - 55
[
new CsrfTokenBadge(
'authenticate',
... line 59
)
]
);
}
... lines 64 - 90
}

esto sólo tiene que coincidir con lo que hayamos utilizado en el formulario. El segundo argumento es el valor enviado, que es $request->request->get() y el nombre de nuestro campo: _csrf_token:

... lines 1 - 22
class LoginFormAuthenticator extends AbstractAuthenticator
{
... lines 25 - 38
public function authenticate(Request $request): PassportInterface
{
... lines 41 - 43
return new Passport(
... lines 45 - 55
[
new CsrfTokenBadge(
'authenticate',
$request->request->get('_csrf_token')
)
]
);
}
... lines 64 - 90
}

Y... ¡ya hemos terminado! Internamente, un oyente se dará cuenta de esta insignia, validará el token CSRF y resolverá la insignia.

¡Vamos a probarlo! Ve a /login, inspecciona el formulario... y encuentra el campo oculto. Ahí está. Introduce cualquier correo electrónico, cualquier contraseña... pero lía el valor del token CSRF. Pulsa "Iniciar sesión" y... ¡sí! ¡Token CSRF inválido! Ahora bien, si no nos metemos con el token... y utilizamos cualquier correo electrónico y contraseña... ¡bien! El token CSRF era válido... así que continuó con el error del correo electrónico.

A continuación: vamos a aprovechar el sistema "recuérdame" de Symfony para que los usuarios puedan permanecer conectados durante mucho tiempo. Esta función también aprovecha el sistema de oyentes y una insignia.

Leave a comment!

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