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!

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