Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

El autentificador y el pasaporte

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

A nivel básico, autenticar a un usuario cuando enviamos el formulario de acceso es... bastante sencillo. Tenemos que leer el email enviado, consultar la base de datos para ese objetoUser... y finalmente comprobar la contraseña del usuario.

La seguridad de Symfony no ocurre en un controlador

Lo raro del sistema de seguridad de Symfony es que... no vamos a escribir esta lógica en el controlador. No. Cuando hagamos un POST a /login, nuestro autentificador va a interceptar esa petición y hará todo el trabajo por sí mismo. Sí, cuando enviemos el formulario de inicio de sesión, nuestro controlador en realidad nunca se ejecutará.

El método supports()

Ahora que nuestro autentificador está activado, al inicio de cada petición, Symfony llamará al método supports() de nuestra clase. Nuestro trabajo es devolver true si esta petición "contiene información de autenticación que sabemos procesar". Si no, devolvemos false. Si devolvemos false, no fallamos en la autenticación: sólo significa que nuestro autenticador no sabe cómo autenticar esta petición... y la petición continúa procesándose con normalidad... ejecutando cualquier controlador con el que coincida.

Así que pensemos: ¿cuándo queremos que nuestro autenticador "haga su trabajo"? ¿Qué peticiones "contienen información de autenticación que sabemos procesar"? La respuesta es: siempre que el usuario envíe el formulario de inicio de sesión.

Dentro de supports() devuelve true si $request->getPathInfo() -es un método elegante para obtener la URL actual- es igual a /login y si $request->isMethod('POST'):

... lines 1 - 11
class LoginFormAuthenticator extends AbstractAuthenticator
{
public function supports(Request $request): ?bool
{
return ($request->getPathInfo() === '/login' && $request->isMethod('POST'));
}
... lines 18 - 43
}

Así que si la petición actual es un POST a /login, queremos intentar autentificar al usuario. Si no, queremos permitir que la petición continúe de forma normal.

Para ver lo que ocurre a continuación, baja en authenticate(), dd('authenticate'):

Tip

PassportInterface está obsoleto desde Symfony 5.4: utiliza en su lugar Passport como tipo de retorno.

... lines 1 - 11
class LoginFormAuthenticator extends AbstractAuthenticator
{
... lines 14 - 18
public function authenticate(Request $request): PassportInterface
{
dd('authenticate!');
}
... lines 23 - 43
}

¡Hora de probar! Ve a actualizar la página de inicio. ¡Sí! El método supports() devolvíafalse... y la página seguía cargándose con normalidad. En la barra de herramientas de depuración de la web, tenemos un nuevo icono de seguridad que dice "Autenticado: no". Pero ahora ve al formulario de inicio de sesión. Esta página sigue cargándose con normalidad. Introduce abraca_admin@example.com -que es el correo electrónico de un usuario real de la base de datos- y una contraseña cualquiera -yo utilizaré foobar-. Envíalo y... ¡lo tienes! ¡Ha llegado a nuestro dd('authenticate')!

El método authenticate()

Así que si supports() devuelve true, Symfony llama a authenticate(). Este es el corazón de nuestro autentificador... y su trabajo es comunicar dos cosas importantes. En primer lugar, quién es el usuario que está intentando iniciar sesión -en concreto, qué objeto deUser es- y, en segundo lugar, alguna prueba de que es ese usuario. En el caso de un formulario de acceso, eso sería una contraseña. Como nuestros usuarios aún no tienen contraseña, la falsificaremos temporalmente.

El objeto Pasaporte: UserBadge y Credenciales

Comunicamos estas dos cosas devolviendo un objeto Passport: return newPassport():

... lines 1 - 12
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
... lines 14 - 15
class LoginFormAuthenticator extends AbstractAuthenticator
{
... lines 18 - 22
public function authenticate(Request $request): PassportInterface
{
... lines 25 - 27
return new Passport(
... lines 29 - 32
);
}
... lines 35 - 55
}

Este simple objeto es básicamente un contenedor de cosas llamadas "insignias"... donde una insignia es un pequeño trozo de información que va en el pasaporte. Las dos insignias más importantes son UserBadge y una especie de "insignia de credenciales" que ayuda a demostrar que este usuario es quien dice ser.

Empieza por coger el correo electrónico y la contraseña que te han enviado:$email = $request->request->get('email'). Si no lo has visto antes,$request->request->get() es la forma de leer los datos de POST en Symfony. En la plantilla de inicio de sesión, el nombre del campo es email... así que leemos el campo POST email. Copia y pega esta línea para crear una variable $password que lea el campopassword del formulario:

... lines 1 - 15
class LoginFormAuthenticator extends AbstractAuthenticator
{
... lines 18 - 22
public function authenticate(Request $request): PassportInterface
{
$email = $request->request->get('email');
$password = $request->request->get('password');
return new Passport(
... lines 29 - 32
);
}
... lines 35 - 55
}

A continuación, dentro del Passport, el primer argumento es siempre el UserBadge. Dinew UserBadge() y pásale nuestro "identificador de usuario". Para nosotros, ese es el $email:

... lines 1 - 10
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
... lines 12 - 15
class LoginFormAuthenticator extends AbstractAuthenticator
{
... lines 18 - 22
public function authenticate(Request $request): PassportInterface
{
$email = $request->request->get('email');
$password = $request->request->get('password');
return new Passport(
new UserBadge($email),
... lines 30 - 32
);
}
... lines 35 - 55
}

Hablaremos muy pronto de cómo se utiliza esto.

El segundo argumento de Passport es una especie de "credencial". Eventualmente le pasaremos un PasswordCredentials().... pero como nuestros usuarios aún no tienen contraseñas, utiliza un nuevo CustomCredentials(). Pásale una devolución de llamada con un argumento $credentialsy un argumento $user de tipo-indicado con nuestra clase User:

... lines 1 - 11
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\CustomCredentials;
... lines 13 - 15
class LoginFormAuthenticator extends AbstractAuthenticator
{
... lines 18 - 22
public function authenticate(Request $request): PassportInterface
{
... lines 25 - 27
return new Passport(
new UserBadge($email),
new CustomCredentials(function($credentials, User $user) {
... lines 31 - 32
);
}
... lines 35 - 55
}

Symfony ejecutará nuestra llamada de retorno y nos permitirá "comprobar las credenciales" de este usuario de forma manual... sea lo que sea que eso signifique en nuestra aplicación. Para empezar, dd($credentials, $user). Ah, y CustomCredentials necesita un segundo argumento, que es cualquiera de nuestras "credenciales". Para nosotros, eso es $password:

... lines 1 - 15
class LoginFormAuthenticator extends AbstractAuthenticator
{
... lines 18 - 22
public function authenticate(Request $request): PassportInterface
{
... lines 25 - 27
return new Passport(
new UserBadge($email),
new CustomCredentials(function($credentials, User $user) {
dd($credentials, $user);
}, $password)
);
}
... lines 35 - 55
}

Si esto de CustomCredentials es un poco confuso, no te preocupes: realmente tenemos que ver esto en acción.

Pero en un nivel alto... es algo genial. Devolvemos un objeto Passport, que dice quién es el usuario -identificado por su email - y una especie de "proceso de credenciales" que probará que el usuario es quien dice ser.

Bien: con sólo esto, vamos a probarlo. Vuelve al formulario de acceso y vuelve a enviarlo. Recuerda: hemos rellenado el formulario con una dirección de correo electrónico que sí existe en nuestra base de datos.

Y... ¡impresionante! foobar es lo que envié para mi contraseña y también está volcando el objeto de entidad User correcto de la base de datos! Así que... ¡oh! De alguna manera, supo consultar el objeto User utilizando ese correo electrónico. ¿Cómo funciona eso?

La respuesta es el proveedor de usuarios Vamos a sumergirnos en eso a continuación, para saber cómo podemos hacer una consulta personalizada para nuestro usuario y terminar el proceso de autenticación.

Leave a comment!

11
Login or Register to join the conversation

Hello, after last step,

return new Passport(
new UserBadge($email),
new CustomCredentials(function($credentials, User $user) {
dd($credentials, $user);
}, $password)
);

I got this error when type Log In

Return value of App\Security\LoginFormAuthenticator::onAuthenticationFailure() must be an instance of Symfony\Component\HttpFoundation\Response or null, none returned

Seems that doesn't enter in new CustomCredentials function and I don't know why, is going directly to onAuthenticationFailure().

Reply

Wooow, I solved! This happened because I didn't type an email that exist on DB when login! That's crazy! I let the question here perhaps helps someone!

Reply

Hey zahariastefan462

That's so awesome that you solved it by yourself! Keep learning and stay in touch!

Cheers!

Reply
Hanane K. Avatar
Hanane K. Avatar Hanane K. | posted hace 1 año

Hello,
in the callback function I type-hinted the argument $user to Symfony\Component\Security\Core\User\User instead of using the entity : App\Entity\User as mentioned in the tutorial, so I had this error Argument 2 passed to App\Security\LoginFormAuthenticator::App\Security\{closure}() must be an instance of Symfony\Component\Security\Core\User\User, instance of App\Entity\User given, called in
In case someone is having same error, just add use App\Entity\User; instead of the one from Symfony\Component\Security\Core\User

Reply

Hey Henane,

Probably you mean Symfony\Component\Security\Core\User\UserInterface that your User entity should implement? Otherwise, make sure your user class extends that "Symfony\Component\Security\Core\User\User" core class if you want to typehint with it :)

Cheers!

Reply
Hanane K. Avatar

Hello,
My User Entity already implements UserInterface, I am talking about the $user argument that we pass to callable of CustomCredentials, with autocompletion I had Symfony\Component\Security\Core\User\User instead of App\Entity\User which result to error mentioned in my previous comment.

Reply

Hey Hanane,

Yes, I understand! :) Just try to re-read my last comment one more time - you either should change your type hint to that "UserInterface" instead of that "Symfony\Component\Security\Core\User\User" class, or you should start extending that "Symfony\Component\Security\Core\User\User" in your User entity - that's how PHP works :) You just can't typehint method arguments with a class or interface and then pass an object that does not extends that class or implements that interface - that won't work, even if your class will have common method names with those classes/interfaces.

Or, it might be so that in the parent method there's already a tyehint with "App\Entity\User" and so you can't downgrade it to a more base class like "Symfony\Component\Security\Core\User\User". But that's only the case if your App\Entity\User class really extends that "Symfony\Component\Security\Core\User\User".

Cheers!

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

By the time you upgrade to a newer system, it's already deprecated again :(( I see PassportInterface is deprecated in 5.4 lol
Edit: it seems the only change is that authenticate() method should return Passport instead of PassportInterface

Reply

Hey The_nuts!

Whoops, that evolves very quickly :) Good catch btw! Yes, you're right, you should use Password instead of PasswordInterface, here's the link to the deprecation message for the reference: https://github.com/symfony/...

We will add a note in this tutorial. Thanks!

Cheers!

1 Reply
Wlc Avatar

What about sanitizing request data?

Reply

Hey wLcDesigns,

Do you have any specific use case? The main job of the authenticator is to say either the current user is authenticated or no, sanitizing might be an overkill, because if the data is corrupted in some way - authenticator will fail anyway.

Cheers!

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": "^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
    }
}