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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeDespué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.
9 Comments
Hi,
I want to ask that you are configuring plainpassword in UseFactory as hard coded. However if I do in AppFixtures as below,
do you suggest this? I want to push User's data from one file.( AppFixtures)
Hey @Mahmut-A
If I correctly understood your question you can just load your file in fixtures and process it like you want, for example read all users with passwords and create them with factory
Cheers!
Hi sadikoff,
I fixed as below: thank you.
` UserFactory::createOne([
Ryan I have very strange one.
I am using symfony forms everywhere and also security is based on your tutorials and documentation for my 5.4, 6.4 projects.
Now, in my new project I am not using symfony forms so I have to implement CSRF protection and here is my question, where I am failing but it seems that it is also issue here and everywhere in my other projects where I am using symfony forms with csrf tokens.
I wanted to test my solution for CSRF implementation if it is working and then I started to question why is it not working like I am expecting so I tested other projects if I am able to replicate it there and it is exactly the same issue so I have this login form and went to developer tools and added "1" in front of _csrf_token value.
I submitted form and it is letting me. Token is valid BUT when I add more characters or change completely _csrf_token value, it will output CSRF Token is Invalid.
I thought it must be perfect match, not just partial or how does it work?
You can try it too. Just clone this code go to developer tools update _csrf_token field value by adding ONE random letter in front of current value
Submit form... it will NOT print CSRF token is invalid but let you progress but if you change completely _csrf_token field value to "Hi,thisisRyan" it will output invalid token so whats the point of implementing CSRF if there does not have to be EXACT match?
Hey @Peter-K!
What an interesting find! I don't know the answer, but:
1) I don't see an attack vector here, even if it is true
2) Though I don't see a "smoking gun", it's possible that the (de)randomization (https://github.com/symfony/symfony/blob/7.3/src/Symfony/Component/Security/Csrf/CsrfTokenManager.php#L118) or splitting on
.(https://github.com/symfony/symfony/blob/7.3/src/Symfony/Component/Security/Csrf/CsrfTokenManager.php#L120) could allow for this.If you dig in further and learn more, I'd love to know!
i am working in project reast api and i don't kow how to implement csrf beccause i have not the parte frondend so what i can do kow?thanks
Hi there!
Sorry for the slow reply! Do your frontend and API live on the same domain name? Or different domain names? Are you using session-based authentication or something different?
Cheers!
How CSRF protection work in Symfony? How app know what CSRF token was send to form and after that validate it ?
Hey!
Somehow we completely missed your comment! Bah - sorry!
The answer is that, when generating a CSRF token, Symfony stores that value in the session. Then, when the user submits the CSRF token, we check that it matches what was in the session. This isn't the only want to do CSRF tokens, but it's the most standard and the one Symfony uses by default.
Cheers!
"Houston: no signs of life"
Start the conversation!