Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

El punto de entrada: invitar a los usuarios a conectarse

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

Vuelve a entrar utilizando abraca_admin@example.com y la contraseña tada. Cuando vamos a/admin, como hemos visto antes, obtenemos "Acceso denegado". Esto se debe a laaccess_control... y al hecho de que nuestro usuario no tiene ROLE_ADMIN.

Pero si lo cambiamos por ROLE_USER -un rol que sí tenemos-, el acceso está garantizado:

security:
... lines 2 - 40
access_control:
- { path: ^/admin, roles: ROLE_USER }
... lines 43 - 44

Y conseguimos ver unos gráficos impresionantes.

Probemos una cosa más. Cerremos la sesión, es decir, vayamos manualmente a /logout. Ahora que no hemos iniciado la sesión, si vamos directamente a /admin: ¿qué debería ocurrir?

Bueno, en este momento, obtenemos una gran página de error con un código de estado 401. Pero... ¡eso no es lo que queremos! Si un usuario anónimo intenta acceder a una página protegida de nuestro sitio, en lugar de un error, queremos ser súper amables e invitarle a iniciar la sesión. Como tenemos un formulario de entrada, significa que queremos redirigir al usuario a la página de entrada.

¡Hola punto de entrada!

Para saber qué hacer cuando un usuario anónimo accede a una página protegida, cada cortafuegos define algo llamado "punto de entrada". El punto de entrada de un cortafuegos es literalmente una función que dice

¡Esto es lo que debemos hacer cuando un usuario anónimo intenta acceder a una página protegida!

Cada autentificador de nuestro cortafuegos puede o no "proporcionar" un punto de entrada. Ahora mismo, tenemos dos autentificadores: nuestro LoginFormAuthenticator personalizado y también el autentificador remember_me:

security:
... lines 2 - 16
firewalls:
... lines 18 - 20
main:
... lines 22 - 23
custom_authenticator: App\Security\LoginFormAuthenticator
... lines 25 - 27
remember_me:
secret: '%kernel.secret%'
signature_properties: [password]
#always_remember_me: true
... lines 32 - 44

Pero ninguno de ellos proporciona un punto de entrada, por lo que, en lugar de redirigir al usuario a una página... o algo diferente, obtenemos este error genérico 401. Algunos autenticadores incorporados -como form_login, del que hablaremos pronto- sí proporcionan un punto de entrada... y lo veremos.

Hacer de nuestro autentificador un punto de entrada

Pero, de todos modos, ninguno de nuestros autenticadores proporciona un punto de entrada... ¡así que vamos a añadir uno!

Abre nuestro autentificador: src/Security/LoginFormAuthenticator.php. Si quieres que tu autentificador proporcione un punto de entrada, todo lo que tienes que hacer es implementar una nueva interfaz: AuthenticationEntryPointInterface:

... lines 1 - 22
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
class LoginFormAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface
{
... lines 27 - 89
}

Esto requiere que la clase tenga un nuevo método... que en realidad ya tenemos aquí abajo. Se llama start(). Descomenta el método. Luego, dentro, muy simplemente, vamos a redirigir a la página de inicio de sesión. Voy a robar el código de arriba:

... lines 1 - 22
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
class LoginFormAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface
{
... lines 27 - 83
public function start(Request $request, AuthenticationException $authException = null): Response
{
return new RedirectResponse(
$this->router->generate('app_login')
);
}
}

¡Y listo!

En cuanto un autentificador implemente esta interfaz, el sistema de seguridad lo notará y empezará a utilizarlo. Literalmente, si un usuario anónimo intenta acceder a una página protegida, ahora llamará a nuestro método start()... y le redirigiremos a la página de inicio de sesión.

Observa: ¡refresca! ¡Bum! Nos lleva a la página de inicio de sesión.

Un cortafuegos tiene exactamente UN punto de entrada

Pero hay una cosa importante que hay que entender sobre los puntos de entrada. Cada cortafuegos sólo puede tener uno. Piensa que en el momento en que entramos en /admin como usuario anónimo.... no estamos intentando entrar a través de un formulario de acceso... o a través de un token de la API. Somos verdaderamente anónimos. Por eso, si tuviéramos varios autentificadores que proporcionaran cada uno un punto de entrada, nuestro cortafuegos no sabría cuál elegir. Necesita un punto de entrada para todos los casos.

Ahora mismo, como sólo uno de nuestros dos autentificadores proporciona un punto de entrada, sabe que debe utilizarlo. Pero, ¿y si no fuera así? Veamos qué pasaría. Busca tu terminal y genera un segundo autentificador personalizado:

symfony console make:auth

Crea un autentificador vacío... y llámalo DummyAuthenticator.

¡Qué bonito! Así se creó una nueva clase llamada DummyAuthenticator:

... lines 1 - 2
namespace App\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
class DummyAuthenticator extends AbstractAuthenticator
{
public function supports(Request $request): ?bool
{
// TODO: Implement supports() method.
}
public function authenticate(Request $request): PassportInterface
{
// TODO: Implement authenticate() method.
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
// TODO: Implement onAuthenticationSuccess() method.
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
// TODO: Implement onAuthenticationFailure() method.
}
// public function start(Request $request, AuthenticationException $authException = null): Response
// {
// /*
// * If you would like this class to control what happens when an anonymous user accesses a
// * protected page (e.g. redirect to /login), uncomment this method and make this class
// * implement Symfony\Component\Security\Http\EntryPoint\AuthenticationEntrypointInterface.
// *
// * For more details, see https://symfony.com/doc/current/security/experimental_authenticators.html#configuring-the-authentication-entry-point
// */
// }
}

Y también actualizó custom_authenticator en security.yaml para utilizar ambas clases personalizadas:

security:
... lines 2 - 16
firewalls:
... lines 18 - 20
main:
... lines 22 - 23
custom_authenticator:
- App\Security\LoginFormAuthenticator
- App\Security\DummyAuthenticator
... lines 27 - 46

En la nueva clase, dentro de supports(), devuelve false:

... lines 1 - 11
class DummyAuthenticator extends AbstractAuthenticator
{
public function supports(Request $request): ?bool
{
return false;
}
... lines 18 - 43
}

No... vamos a convertir esto en un autentificador real.

Si nos detuviéramos ahora mismo... e intentáramos ir a /admin, seguiría utilizando el punto de entrada de LoginFormAuthenticator. Pero ahora implementaAuthenticationEntryPointInterface:

... lines 1 - 10
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
class DummyAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface
{
... lines 15 - 38
}

Y luego baja... y descomenta el método start(). Para el cuerpo, sólo dd()un mensaje... porque esto no se ejecutará nunca:

... lines 1 - 12
class DummyAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface
{
... lines 15 - 34
public function start(Request $request, AuthenticationException $authException = null): Response
{
dd('DummyAuthenticator::start()!');
}
}

Ahora el cortafuegos se dará cuenta de que tenemos dos autentificadores que proporcionan un punto de entrada cada uno. Y así, cuando refresquemos cualquier página, entrará en pánico. El error dice

¡Ejecuta por ti liiiiii!

Oh, espera, en realidad dice

Como tienes varios autentificadores en el cortafuegos "principal", tienes que establecer la entry_point clave a uno de tus autentificadores.

Y nos dice, de forma muy útil, los dos autentificadores que tenemos. En otras palabras: nos hace elegir.

Copia la clave entry_point... y luego, en cualquier lugar del cortafuegos, dientry_point: y apunta al servicio LoginFormAuthenticator:

security:
... lines 2 - 16
firewalls:
... lines 18 - 20
main:
... lines 22 - 23
entry_point: App\Security\LoginFormAuthenticator
... lines 25 - 47

Técnicamente podemos apuntar a cualquier servicio que implementeAuthenticationEntryPointInterface... pero normalmente lo pongo en mi autentificador.

Ahora... si volvemos a /admin.... ¡funciona bien! Sabe que debe elegir el punto de entrada de LoginFormAuthenticator.

Hablando de LoginFormAuthenticator... um... ¡hemos estado haciendo demasiado trabajo dentro de él! Eso es culpa mía - estamos haciendo las cosas de la manera más difícil para... ya sabes... ¡"aprender"! Pero a continuación, vamos a eliminar eso y aprovechar una clase base de Symfony que nos permitirá eliminar un montón de código. También vamos a aprender algo llamado TargetPathTrait: una forma inteligente de redirigir al usuario en caso de éxito.

Leave a comment!

6
Login or Register to join the conversation
Default user avatar
Default user avatar Nicolas | posted hace 1 año

I have an issue with what I tried to do.
On my website I have an admin area and a public one. On the public side, by configuration, I can let user access all pages without authentication or require them an account if public side is private.
To manage that purpose I have a special account (guest in my database) with parameters (preferred language, theme, ...) like others users but theses parameters can only be changed by the webmaster.

To simplify my code (in controllers, services, templates,...) there's no difference between special account (guest) and other users. If guest access is allowed by webmaster, I want users to be automatically connected. So I create a new authenticator (GuestAuthenticator) with SelfValidatingPassport. It works.

It seems to works but... When I visit a page as guest and I try to access admin area I want to be redirect to login page. My first idea was that my authenticator do not implement AuthenticationEntryPointInterface. But the problem is that the returned response is 403 "Access Denied" because the user is already authenticated.

The main problem I try to resolve is that anonymous user are not real user in symfony but only a string. If you have another idea for me, it will be great.

If it's not clear, just tell me.

p.s: like always a really great tuorial

Reply

Hey Nicolas,

As far as I understand, you want to redirect "guest" users to the login page as soon as they try to access your admin area. Unfortunately, as soon as your guest user is authenticated in Symfony - you won't be redirect to the login page because Symfony already knows who you are and knows your roles. If you don't have a required role - the access will be denied with the error you mentioned instead of redirecting you to login page. So, either you should not authenticate anonymous users in your system and then the redirect will work, or accept that your users will see 403 access deny error accessing resources they don't have access to. Fairly speaking, I'm not sure how you can bypass this, that is pretty complex security system behind the scene. Maybe some even listeners with correct priority may help you, but I'm not sure, sorry.

Cheers!

Reply
Default user avatar

Thanks Victor for your qucik answer.

You perfectly understand.

Before symfony 5.3, I used an ugly hack, overriding getUser provided by AbstractControl. My getUser() method return a real User even for anonymous user. The token for anonymous user was AnonymousToken.

Until I find a better solution, guest users wil see a 403 deny access if they try to access admin area.

Reply

Hey Nicolas,

Yeah, an interesting hack actually, I suppose you can do something similar too. But instead of overriding getUser() in the abstract controller (because this way it will work only in controllers), I'd recommend you to create a separate service for this, where you will inject Security service. And if the Security service return null, i.e. if the user is anon - then return a Guest object in your custom service instead. Then, everywhere, instead of using Security service, you would need to use your new custom service that will return either Guest object or a User object, depends on whether the authenticated fully or anonymously. And this way I suppose you don't need a custom authenticator for Guest users at all, just main authenticated that authenticated users.

I hope this helps.

Cheers!

Reply
Default user avatar

It helps a lot. A custom service to retrieve the right user is a really good idea. Thanks

Reply

Hey Nicolas,

Great, I'm happy to hear it was useful for you :) And then you can override the getUser() in your base controller and leverage this custom service as well to avoid code duplication ;)

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