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.
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.
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
.
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
.
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.
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.
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!
// 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
}
}
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