Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Consulta de usuario personalizada y credenciales

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

En la pantalla, vemos un dd() de la contraseña que introduje en el formulario de acceso y el objeto de entidad User para el correo electrónico que introduje. ¡Algo, de alguna manera, supo tomar el correo electrónico introducido y consultar por el Usuario!

UserBadge y el proveedor de usuarios

Así es como funciona esto. Después de que devolvamos el objeto Passport, el sistema de seguridad intenta encontrar el objeto User a partir de UserBadge. Si sólo le pasas un argumento a UserBadge -como es nuestro caso-, lo hace aprovechando nuestro proveedor de usuarios. ¿Recuerdas esa cosa de security.yaml llamada providers?

security:
... lines 2 - 7
providers:
# used to reload user from session & other features (e.g. switch_user)
app_user_provider:
entity:
class: App\Entity\User
property: email
... lines 14 - 34

Como nuestra clase User es una entidad, estamos utilizando el proveedor entity que sabe cómo cargar usuarios utilizando la propiedad email. Así que, básicamente, se trata de un objeto que es muy bueno para consultar la tabla de usuarios a través de la propiedad email. Así que cuando pasamos sólo el correo electrónico a UserBadge, el proveedor de usuarios lo utiliza para consultar User.

Si se encuentra un objeto User, Symfony intenta entonces "comprobar las credenciales" de nuestro pasaporte. Como estamos utilizando CustomCredentials, esto significa que ejecuta esta llamada de retorno... en la que volcamos algunos datos. Si no se encuentra un User - porque hemos introducido un correo electrónico que no está en la base de datos - la autenticación falla. Pronto veremos más sobre estas dos situaciones.

Consulta de usuario personalizada

En cualquier caso, la cuestión es la siguiente: si sólo pasas un argumento a UserBadge, el proveedor de usuarios carga el usuario automáticamente. Eso es lo más fácil de hacer. E incluso puedes personalizar un poco esta consulta si lo necesitas - busca "Usar una consulta personalizada para cargar el usuario" en los documentos de Symfony para ver cómo hacerlo.

O... puedes escribir tu propia lógica personalizada para cargar el usuario aquí mismo. Para ello, vamos a necesitar el UserRepository. En la parte superior de la clase, añadepublic function __construct()... y autoconduce un argumento UserRepository. PulsaréAlt+Enter y seleccionaré "Inicializar propiedades" para crear esa propiedad y establecerla:

... lines 1 - 5
use App\Repository\UserRepository;
... lines 7 - 17
class LoginFormAuthenticator extends AbstractAuthenticator
{
private UserRepository $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
... lines 26 - 73
}

En authenticate(), UserBadge tiene un segundo argumento opcional llamado cargador de usuario. Pásale una llamada de retorno con un argumento: $userIdentifier:

... lines 1 - 17
class LoginFormAuthenticator extends AbstractAuthenticator
{
... lines 20 - 31
public function authenticate(Request $request): PassportInterface
{
... lines 34 - 36
return new Passport(
new UserBadge($email, function($userIdentifier) {
... lines 39 - 46
}),
... lines 48 - 50
);
}
... lines 53 - 73
}

Es bastante sencillo: si le pasas un callable, cuando Symfony cargue tu User, llamará a esta función en lugar de a tu proveedor de usuario. Nuestro trabajo aquí es cargar el usuario y devolverlo. El $userIdentifier será lo que hayamos pasado al primer argumento de UserBadge... así que el email en nuestro caso.

Digamos que $user = $this->userRepository->findOneBy() para consultar email se ajusta a$userIdentifier:

... lines 1 - 17
class LoginFormAuthenticator extends AbstractAuthenticator
{
... lines 20 - 31
public function authenticate(Request $request): PassportInterface
{
... lines 34 - 36
return new Passport(
new UserBadge($email, function($userIdentifier) {
// optionally pass a callback to load the User manually
$user = $this->userRepository->findOneBy(['email' => $userIdentifier]);
... lines 41 - 46
}),
... lines 48 - 50
);
}
... lines 53 - 73
}

Aquí es donde puedes utilizar cualquier consulta personalizada que quieras. Si no podemos encontrar al usuario, tenemos que lanzar una excepción especial. Así que si no es $user,throw new UserNotFoundException(). Eso hará que falle la autenticación. En la parte inferior, devuelve $user:

... lines 1 - 17
class LoginFormAuthenticator extends AbstractAuthenticator
{
... lines 20 - 31
public function authenticate(Request $request): PassportInterface
{
... lines 34 - 36
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;
}),
... lines 48 - 50
);
}
... lines 53 - 73
}

Esto... es básicamente idéntico a lo que hacía nuestro proveedor de usuarios hace un minuto... así que no cambiará nada. Pero puedes ver que tenemos el poder de cargar elUser como queramos.

Actualicemos. Sí El mismo volcado que antes.

Validación de las credenciales

Bien, si se encuentra un objeto User - ya sea desde nuestro callback personalizado o desde el proveedor de usuarios - Symfony comprueba a continuación nuestras credenciales, lo que significa algo diferente dependiendo del objeto de credenciales que pases. Hay 3 principales:PasswordCredentials - lo veremos más adelante, un SelfValidatingPassport que sirve para la autenticación de la API y no necesita ninguna credencial - y CustomCredentials.

Si usas CustomCredentials, Symfony ejecuta la llamada de retorno... y nuestro trabajo es "comprobar sus credenciales"... sea lo que sea que eso signifique en nuestra aplicación. El argumento $credentialscoincidirá con lo que hayamos pasado al segundo argumento de CustomCredentials. Para nosotros, eso es la contraseña enviada:

... lines 1 - 17
class LoginFormAuthenticator extends AbstractAuthenticator
{
... lines 20 - 31
public function authenticate(Request $request): PassportInterface
{
... lines 34 - 36
return new Passport(
... lines 38 - 47
new CustomCredentials(function($credentials, User $user) {
... line 49
}, $password)
);
}
... lines 53 - 73
}

¡Imaginemos que todos los usuarios tienen la misma contraseña tada! Para validarlo, devuelve true si $credentials === 'tada':

... lines 1 - 17
class LoginFormAuthenticator extends AbstractAuthenticator
{
... lines 20 - 31
public function authenticate(Request $request): PassportInterface
{
... lines 34 - 36
return new Passport(
... lines 38 - 47
new CustomCredentials(function($credentials, User $user) {
return $credentials === 'tada';
}, $password)
);
}
... lines 53 - 73
}

¡Seguridad hermética!

Fallo y éxito de la autenticación

Si devolvemos true desde esta función, ¡la autenticación ha sido un éxito! ¡Vaya! Si devolvemos false, la autenticación falla. Para comprobarlo, baja a onAuthenticationSuccess() y dd('success'). Haz lo mismo dentro de onAuthenticationFailure():

... lines 1 - 17
class LoginFormAuthenticator extends AbstractAuthenticator
{
... lines 20 - 53
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
dd('success');
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
dd('failure');
}
... lines 63 - 73
}

Pronto pondremos código real en estos métodos... pero su propósito se explica por sí mismo: si la autenticación tiene éxito, Symfony llamará a onAuthenticationSuccess(). Si la autenticación falla por cualquier motivo - como un correo electrónico o una contraseña no válidos - Symfony llamará a onAuthenticationFailure().

¡Vamos a probarlo! Vuelve directamente a /login. Utiliza de nuevo el correo electrónico real -abraca_admin@example.com con la contraseña correcta: tada. Envía y... ¡sí! llamó a onAuthenticationSuccess(). ¡La autenticación se ha completado!

Lo sé, todavía no parece gran cosa... así que a continuación, vamos a hacer algo en caso de éxito, como redirigir a otra página. También vamos a conocer el otro trabajo crítico de un proveedor de usuarios: refrescar el usuario de la sesión al principio de cada petición para mantenernos conectados.

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