Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Autenticación manual

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

Nuestro formulario de registro funcionaría si lo probamos. Pero, tras el registro, quiero también autenticar automáticamente al usuario... para que no tenga que registrarse y luego iniciar inmediatamente la sesión... eso sería una tontería.

Hasta ahora, toda la autentificación se ha hecho... de forma indirecta: el usuario hace una petición, algún autentificador la gestiona y... ¡voilà! Pero en este caso, queremos autenticar al usuario directamente, escribiendo código dentro de un controlador.

Hola UserAuthenticatorInterface

Y... esto es totalmente posible, autocableando un servicio específicamente para esto. Añade un nuevo argumento aquí arriba de tipo UserAuthenticatorInterface y lo llamaré $userAuthenticator:

... lines 1 - 11
use Symfony\Component\Security\Http\Authentication\UserAuthenticatorInterface;
class RegistrationController extends AbstractController
{
... lines 16 - 18
public function register(Request $request, UserPasswordHasherInterface $userPasswordHasher, UserAuthenticatorInterface $userAuthenticator): Response
{
... lines 21 - 48
}
}

Este objeto te permite simplemente... autentificar cualquier objeto de User. Justo antes de la redirección, vamos a hacer eso: $userAuthenticator->authenticateUser() y necesitamos pasarle a esto unos cuantos argumentos. El primero es el User que queremos autenticar:

... lines 1 - 13
class RegistrationController extends AbstractController
{
... lines 16 - 18
public function register(Request $request, UserPasswordHasherInterface $userPasswordHasher, UserAuthenticatorInterface $userAuthenticator): Response
{
... lines 21 - 24
if ($form->isSubmitted() && $form->isValid()) {
... lines 26 - 38
$userAuthenticator->authenticateUser(
$user,
);
return $this->redirectToRoute('app_homepage');
}
... lines 45 - 48
}
}

Fácil. El segundo es un "autentificador" que quieres utilizar. Este sistema funciona básicamente tomando tu objeto User y... como "pasándolo por" uno de tus autentificadores.

Si siguiéramos utilizando nuestro LoginFormAuthenticator personalizado, pasar este argumento sería realmente fácil. Podríamos simplemente autoconectar el servicio LoginFormAuthenticator aquí arriba y pasarlo.

Inyectar el servicio para form_login

Pero, en nuestro archivo security.yaml, nuestra principal forma de autenticación es form_login:

security:
... lines 2 - 20
firewalls:
... lines 22 - 24
main:
... lines 26 - 28
form_login:
login_path: app_login
check_path: app_login
username_parameter: email
password_parameter: password
enable_csrf: true
... lines 35 - 61

Esto activa un servicio de autenticación entre bastidores, al igual que nuestro LoginFormAuthenticator personalizado. La parte complicada es averiguar cuál es ese servicio e inyectarlo en nuestro controlador.

Así que tenemos que investigar un poco. En tu terminal, ejecuta

symfony console debug:container

y busca form_login:

symfony console debug:container form_login

En esta lista, veo un servicio llamado security.authenticator.form_login.main... y recuerda que "main" es el nombre de nuestro cortafuegos. Este es el id del servicio que queremos. Si te preguntas por el servicio que hay encima de esto, si lo compruebas, verás que es un servicio "abstracto". Una especie de servicio "falso" que se utiliza como plantilla para crear el servicio real para cualquier cortafuegos que utiliceform_login.

En cualquier caso, voy a pulsar "1" para obtener más detalles. Vale, genial: este servicio es una instancia deFormLoginAuthenticator, que es la clase principal que hemos visto antes.

De vuelta a nuestro controlador, añade otro argumento de tipo FormLoginAuthenticator:

... lines 1 - 12
use Symfony\Component\Security\Http\Authenticator\FormLoginAuthenticator;
class RegistrationController extends AbstractController
{
... lines 17 - 19
public function register(Request $request, UserPasswordHasherInterface $userPasswordHasher, UserAuthenticatorInterface $userAuthenticator, FormLoginAuthenticator $formLoginAuthenticator): Response
{
... lines 22 - 51
}
}

Luego, aquí abajo, pasa el nuevo argumento a authenticateUser():

... lines 1 - 14
class RegistrationController extends AbstractController
{
... lines 17 - 19
public function register(Request $request, UserPasswordHasherInterface $userPasswordHasher, UserAuthenticatorInterface $userAuthenticator, FormLoginAuthenticator $formLoginAuthenticator): Response
{
... lines 22 - 25
if ($form->isSubmitted() && $form->isValid()) {
... lines 27 - 39
$userAuthenticator->authenticateUser(
$user,
$formLoginAuthenticator,
... line 43
);
... lines 45 - 46
}
... lines 48 - 51
}
}

Esto no funcionará todavía, pero sigue conmigo.

El argumento final a authenticateUser() es el objeto Request... que ya tenemos... es nuestro primer argumento del controlador:

... lines 1 - 14
class RegistrationController extends AbstractController
{
... lines 17 - 19
public function register(Request $request, UserPasswordHasherInterface $userPasswordHasher, UserAuthenticatorInterface $userAuthenticator, FormLoginAuthenticator $formLoginAuthenticator): Response
{
... lines 22 - 25
if ($form->isSubmitted() && $form->isValid()) {
... lines 27 - 39
$userAuthenticator->authenticateUser(
$user,
$formLoginAuthenticator,
$request
);
... lines 45 - 46
}
... lines 48 - 51
}
}

authenticateUser devuelve una respuesta

¡Ya está! ¡Ah, y una cosa genial de authenticateUser() es que devuelve un objetoResponse! En concreto, el objeto Response del métodoonAuthenticationSuccess() de cualquier autentificador que hayamos pasado. Esto significa que, en lugar de redirigir a la página de inicio, podemos devolver esto y, dondequiera que ese autentificador redirija normalmente, redirigiremos allí también, que podría ser la "ruta de destino".

Vinculación del servicio form_login

¡Vamos a probar esto! Actualiza el formulario de registro para ser recibido con... ¡un impresionante error!

No se puede autoenlazar el argumento $formLoginAuthenticator.

Hmm. Sí que hemos tecleado ese argumento con la clase correcta:FormLoginAuthenticator:

... lines 1 - 12
use Symfony\Component\Security\Http\Authenticator\FormLoginAuthenticator;
class RegistrationController extends AbstractController
{
... lines 17 - 19
public function register(Request $request, UserPasswordHasherInterface $userPasswordHasher, UserAuthenticatorInterface $userAuthenticator, FormLoginAuthenticator $formLoginAuthenticator): Response
{
... lines 22 - 51
}
}

¡El problema es que éste es un raro ejemplo de un servicio que no está disponible para el autocableado! Así que tenemos que configurarlo manualmente.

Afortunadamente, si no sabíamos ya qué servicio necesitamos, el mensaje de error nos da una gran pista. Dice:

... no existe tal servicio, tal vez debas asignar un alias de esta clase al servicio existente security.authenticator.form_login.main

Sí, nos ha dado el id del servicio que necesitamos cablear.

Ve a copiar el nombre del argumento - formLoginAuthenticator - y luego abreconfig/services.yaml. Debajo de _defaults, añade un nuevo bind llamado$formLoginAuthenticator ajustado a @ y luego... Iré a copiar ese largo id de servicio... y lo pegaré aquí:

... lines 1 - 8
services:
# default configuration for services in *this* file
_defaults:
... lines 12 - 13
bind:
... line 15
$formLoginAuthenticator: '@security.authenticator.form_login.main'
... lines 17 - 32

Esto dice: siempre que un servicio tenga un argumento $formLoginAuthenticator, pásale este servicio.

Eso... si refrescamos... eliminará nuestro error.

Bien, ¡registremos por fin un nuevo usuario! Utilizaré mi correo electrónico de la vida real... y luego cualquier contraseña... siempre que tenga 6 caracteres: nuestro formulario de registro venía preconstruido con esa regla de validación. Y... lo tenemos. Abajo, en la barra de herramientas de depuración de la web, ¡estamos registrados como Merlín! Siento el poder mágico.

Siguiente: a veces denegar el acceso no es tan sencillo como comprobar un rol. Por ejemplo, ¿qué pasaría si tuvieras una página de edición de preguntas y ésta tuviera que ser accesible sólo para el creador de esa pregunta? Es el momento de descubrir un poderoso sistema dentro de Symfony llamado votantes.

Leave a comment!

13
Login or Register to join the conversation
Marc-D Avatar

Hi Ryan !

Another great tutorial.

I probably have a stupid question : What's the point of using FormLoginAuthenticator $formLoginAuthenticator authenticator when we could use our previous LoginFormAuthenticator authenticator ?

This would be something like :

 public function register(Request $request, UserPasswordHasherInterface $userPasswordHasher, EntityManagerInterface $entityManager, UserAuthenticatorInterface $userAuthenticator,UserRepository $userRepository, RouterInterface $router ): Response
    {
        $user = new User();
        $form = $this->createForm(RegistrationFormType::class, $user);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {

            [..]            

            // do anything else you need here, like send an email

            return $userAuthenticator->authenticateUser(
                $user,
                new LoginFormAuthenticator($userRepository, $router),
                $request
            );
        }

        return $this->render('registration/register.html.twig', [
            'registrationForm' => $form->createView(),
        ]);
    }

Thanks,

Marc.

Reply

Hey Marc,

The UserAuthenticator has a different responsibility than our custom LoginFormAuthenticator. Its job is to actually log in users into the system. As you can see, we're not making our users go through the login form after registration, so the UserAuthenticator knows nothing about how to authenticate a request (validate credentials, etc)

Cheers!

Reply
discipolat Avatar
discipolat Avatar discipolat | posted hace 1 año | edited

Hi there. What would you suggest for authentication with the WorkFlow component. The idea, in the registration form, user will provide some docs (id card...) and submit, the admin will then receive an notification and analyse the submitted item and decide to fully athenticate the user or not.

Reply
discipolat Avatar

I'm planning just to add a new property to the User entity..like isValidated, boolean. Then, throught the WorkFlow component set that property to true when the admin accept the details sent throught the form. Is that correct !? Or what would you suggest?

Reply

Hey discipolat!

Sorry for the slow reply! That sounds very reasonable to me. If it's JUST this simple, I might not use the Workflow component, but that component is great, so I have no problems with it.

Does this help? Or did you also want to, somehow, authenticate the user the moment that the admin approves them?

Cheers!

Reply
discipolat Avatar

Very helpful!
Yes you're wright indeed.
yes, just after the approvol authenticate the user.

Reply
Default user avatar

The "Show Lines" feature seems to be broken in some code blocks on this page. For example try to load "... lines 1 - 11" in the first code block. Can you please fix this?

Reply

FYI codeblocks expanding back! Woohoo! Thank you for the reporting!

Reply

Hey @Guido

Sorry for that, We are working to solve this issue, soon this feature will come back!

Cheers and stay tuned!

Reply
Matteo S. Avatar
Matteo S. Avatar Matteo S. | posted hace 1 año

At 4:32 I get the expected error "Cannot autowire ..." but it lacks the "Perhaps you should alias this class to..." part. Why is that?

Reply

Hey Matteo S.!

Hmm. Ok, here's how this works behind the scenes - it may help spot why you see the difference:

A) Symfony sees the type-hint for Symfony\Component\Security\Http\Authenticator\FormLoginAuthenticator that it can't autowire.
B) To give you the best error message, it then loops over every service in the container to see if any services have this class.

If a matching service (or services) ARE found, it'll give you that alias type-hint. If none are found, it won't. In this situation, I had the security.authenticator.form_login.main service because I'm using the form_login authenticator under my "main" firewall. So my first guess would be that you maybe aren't using form_login under your firewall? If you are, then yes, I would also expect you to see the exact same message as me :).

Cheers!

1 Reply
Matteo S. Avatar

Thank you! Indeed, turns out I'm not using form_login, but rather "custom_authenticator: App\Security\LoginFormAuthenticator" which I seem to remember was created by the Maker bundle.

Reply

Sweet! That explains it!

And yes, MakerBundle still generates a custom authenticator in all cases. I'm thinking we should change that by asking you a further question so we can determine if you really *need* a custom authenticator, or if we can just hook you up with form_login.

Cheers!

1 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": ">=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
    }
}
userVoice