Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Éxito de la autenticación y actualización del usuario

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

Hagamos un rápido repaso de cómo funciona nuestro autentificador. Después de activarlo en security.yaml:

security:
... lines 2 - 13
firewalls:
... lines 15 - 17
main:
... lines 19 - 20
custom_authenticator: App\Security\LoginFormAuthenticator
... lines 22 - 34

Symfony llama a nuestro método supports() en cada petición antes del controlador:

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

Como nuestro autentificador sabe cómo manejar el envío del formulario de inicio de sesión, devolvemos true si la petición actual es un POST a /login. Una vez que devolvemos true, Symfony llama a authenticate() y básicamente pregunta:

Bien, dime quién está intentando iniciar sesión y qué prueba tiene.

Respondemos a estas preguntas devolviendo un Passport:

... 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;
}),
new CustomCredentials(function($credentials, User $user) {
return $credentials === 'tada';
}, $password)
);
}
... lines 53 - 73
}

El primer argumento identifica al usuario y el segundo argumento identifica alguna prueba... en este caso, sólo una devolución de llamada que comprueba que la contraseña enviada es tada. Si somos capaces de encontrar un usuario y las credenciales son correctas... ¡entonces estamos autentificados!

¡Ya lo vimos al final del último vídeo! Cuando iniciamos la sesión utilizando el correo electrónico de un usuario real en nuestra base de datos y la contraseña tada... golpeamos esta declaración dd():

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

onAuthenticationSuccess

Si la autenticación tiene éxito, Symfony llama a onAuthenticationSuccess() y pregunta:

¡Felicidades por la autenticación! ¡Estamos súper orgullosos! Pero... ¿qué debemos hacer ahora?

En nuestra situación, después del éxito, probablemente queramos redirigir al usuario a alguna otra página. Pero para otros tipos de autenticación podrías hacer algo diferente. Por ejemplo, si te estás autenticando mediante un token de la API, devolverías nulldesde este método para permitir que la petición continúe hacia el controlador normal.

En cualquier caso, ese es nuestro trabajo aquí: decidir qué hacer "a continuación"... que será "no hacer nada" - null - o devolver algún tipo de objeto Response. Vamos a redirigir.

Dirígete a la parte superior de esta clase. Añade un segundo argumento -RouterInterface $router - utiliza el truco Alt+Enter y selecciona "Inicializar propiedades" para crear esa propiedad y establecerla:

... lines 1 - 9
use Symfony\Component\Routing\RouterInterface;
... lines 11 - 19
class LoginFormAuthenticator extends AbstractAuthenticator
{
... line 22
private RouterInterface $router;
public function __construct(UserRepository $userRepository, RouterInterface $router)
{
... line 27
$this->router = $router;
}
... lines 30 - 79
}

De vuelta a onAuthenticationSuccess(), necesitamos devolver null o un Response. Devuelve un nuevo RedirectResponse() y, para la URL, di $this->router->generate()y pasa app_homepage:

... lines 1 - 6
use Symfony\Component\HttpFoundation\RedirectResponse;
... lines 8 - 19
class LoginFormAuthenticator extends AbstractAuthenticator
{
... lines 22 - 57
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
return new RedirectResponse(
$this->router->generate('app_homepage')
);
}
... lines 64 - 79
}

Déjame ir... vuelve a comprobar que el nombre de la ruta .... debe estar dentro deQuestionController. Sí, app_homepage es correcta:

... lines 1 - 17
class QuestionController extends AbstractController
{
... lines 20 - 29
/**
* @Route("/{page<\d+>}", name="app_homepage")
*/
public function homepage(QuestionRepository $repository, int $page = 1)
{
... lines 35 - 43
}
... lines 45 - 86
}

No estoy seguro de por qué PhpStorm cree que falta esta ruta... definitivamente está ahí.

De todos modos, vamos a entrar desde cero. Vamos directamente a /login, introducimosabraca_admin@example.com - porque es un correo electrónico real en nuestra base de datos - y la contraseña "tada". Cuando enviamos... ¡funciona! ¡Somos redirigidos! ¡Y estamos conectados! Lo sé gracias a la barra de herramientas de depuración de la web: conectado como abraca_admin@example.com, autentificado: Sí.

Si haces clic en este icono para entrar en el perfil, hay un montón de información jugosa sobre la seguridad. Vamos a hablar de las partes más importantes de esto a medida que avancemos.

Información sobre la autenticación y la sesión

Vuelve a la página de inicio. Fíjate en que, si navegamos por el sitio, seguimos conectados... que es lo que queremos. Esto funciona porque los cortafuegos de Symfony son, por defecto, "stateful". Es una forma elegante de decir que, al final de cada petición, el objeto User se guarda en la sesión. Luego, al inicio de la siguiente petición, ese objeto User se carga desde la sesión... y seguimos conectados.

Actualizar el usuario

¡Esto funciona muy bien! Pero... hay un problema potencial. Imagina que nos conectamos en el ordenador del trabajo. Luego, nos vamos a casa, iniciamos la sesión en un ordenador totalmente diferente y cambiamos algunos de nuestros datos de usuario, como por ejemplo, cambiamos nuestro firstName en la base de datos a través de una sección de "edición de perfil". Cuando volvamos al trabajo al día siguiente y actualicemos el sitio, Symfony cargará, por supuesto, el objeto User de la sesión. Pero... ¡ese objeto User tendrá ahora el firstName equivocado! Sus datos ya no coincidirán con lo que hay en la base de datos... porque estamos recargando un objeto "viejo" de la sesión.

Afortunadamente... esto no es un problema real. ¿Por qué? Porque al principio de cada petición, Symfony también refresca el usuario. Bueno, en realidad nuestro "proveedor de usuarios" hace esto. Volviendo a security.yaml, ¿recuerdas esa cosa del proveedor de usuarios?

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
firewalls:
... lines 15 - 17
main:
... line 19
provider: app_user_provider
... lines 21 - 34

Sí, tiene dos funciones. En primer lugar, si le damos un correo electrónico, sabe cómo encontrar a ese usuario. Si sólo le pasamos un único argumento a UserBadge, el proveedor de usuarios hace el trabajo duro de cargar el User desde la base de datos:

... lines 1 - 19
class LoginFormAuthenticator extends AbstractAuthenticator
{
... lines 22 - 35
public function authenticate(Request $request): PassportInterface
{
... lines 38 - 40
return new Passport(
new UserBadge($email, function($userIdentifier) {
... lines 43 - 50
}),
... lines 52 - 54
);
}
... lines 57 - 79
}

Pero el proveedor de usuarios también tiene un segundo trabajo. Al comienzo de cada petición, refresca el User consultando la base de datos para obtener datos nuevos. Todo esto ocurre automáticamente en segundo plano.... ¡lo cual es genial! Es un proceso aburrido, pero crítico, del que tú, al menos, deberías ser consciente.

Cambio de usuario === Cierre de sesión

Ah, y por cierto: después de consultar los datos frescos de User, si algunos datos importantes del usuario han cambiado -como los de email, password o roles - se te cerrará la sesión. Se trata de una función de seguridad: permite que un usuario, por ejemplo, cambie su contraseña y haga que se cierre la sesión de cualquier usuario "malo" que haya podido acceder a su cuenta. Si quieres saber más sobre esto, buscaEquatableInterface: es una interfaz que te permite controlar este proceso.

Averigüemos qué ocurre cuando falla la autenticación. ¿Dónde va el usuario? ¿Cómo se muestran los errores? ¿Cómo vamos a tratar la carga emocional del fracaso? La mayor parte de eso es lo siguiente.

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