If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeEs hora de arreglar estas depreciaciones para que finalmente podamos actualizar a Symfony 6. Ve a cualquier página del sitio y haz clic en las deprecaciones de la barra de herramientas de depuración web para ver la lista. Es una lista grande... pero muchas de ellas están relacionadas con lo mismo: la seguridad.
El mayor cambio -y quizás el más maravilloso- en Symfony 5.4 y Symfony 6, es el nuevo sistema de seguridad. Pero no te preocupes. No es muy diferente del antiguo... y la ruta de actualización es sorprendentemente fácil.
Para el primer cambio, abre la entidad User
. Además de UserInterface
, añade un segundo PasswordAuthenticatedUserInterface
. Hasta hace poco, UserInterface
tenía un montón de métodos, entre ellos getPassword()
... lines 1 - 8 | |
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; | |
... lines 10 - 14 | |
class User implements UserInterface, PasswordAuthenticatedUserInterface | |
{ | |
... lines 17 - 211 | |
} |
Pero... esto no siempre tenía sentido. Por ejemplo, algunos sistemas de seguridad tienen usuarios que no tienen contraseñas. Por ejemplo, si tus usuarios se conectan a través de un sistema de inicio de sesión único, entonces no hay contraseñas que manejar. Bien, el usuario puede introducir su contraseña en ese sistema... pero en lo que respecta a nuestra aplicación, no hay contraseñas.
Para hacer esto más limpio, en Symfony 6, se eliminó getPassword()
deUserInterface
. Así que siempre tienes que implementar UserInterface
... pero entonces el método getPassword()
y su PasswordAuthenticatedUserInterface
son opcionales.
Otro cambio se refiere a getUsername()
. Este método vive en UserInterface
... pero su nombre siempre era confuso. Hacía parecer que era necesario tener un nombre de usuario... cuando en realidad, este método se supone que devuelve cualquier identificador de usuario único, no necesariamente un nombre de usuario. Por eso, en Symfony 6, se ha cambiado el nombre de getUsername()
a getUserIdentifier()
. Copia esto, pégalo, cambia getUsername
por getUserIdentifier()
... y ya está.
... lines 1 - 14 | |
class User implements UserInterface, PasswordAuthenticatedUserInterface | |
{ | |
... lines 17 - 69 | |
/** | |
* A visual identifier that represents this user. | |
* | |
* @see UserInterface | |
*/ | |
public function getUserIdentifier(): string | |
{ | |
return (string) $this->email; | |
} | |
... lines 79 - 221 | |
} |
Por ahora tenemos que mantener getUsername()
porque todavía estamos en Symfony 5.4... pero una vez que actualicemos a Symfony 6, podemos eliminarlo con seguridad.
Pero el mayor cambio en el sistema de seguridad de Symfony se encuentra enconfig/packages/security.yaml
. Es este enable_authenticator_manager
. Cuando actualizamos la receta, nos dio esta configuración... pero estaba establecida en true
security: | |
... lines 2 - 9 | |
enable_authenticator_manager: false | |
... lines 11 - 64 |
Esta pequeñísima línea de aspecto inocente nos permite cambiar del antiguo sistema de seguridad al nuevo. Y lo que esto significa, en la práctica, es que todas las formas de autenticación -como un autentificador personalizado o form_login
o http_basic
- comenzarán de repente a utilizar un sistema completamente nuevo bajo el capó.
En su mayor parte, si utilizas uno de los sistemas de autenticación integrados, como form_login
o http_basic
... probablemente no notarás ningún cambio. Puedes activar el nuevo sistema estableciendo esto como verdadero... y todo funcionará exactamente como antes.... aunque el código detrás de form_login
será de repente muy diferente. En muchos sentidos, el nuevo sistema de seguridad es una refactorización interna para hacer el código del núcleo más legible y para darnos más flexibilidad, cuando la necesitemos.
Sin embargo, si tienes algún autentificador personalizado de guard
... como nosotros, tendrás que convertirlo al nuevo sistema de autentificadores... que de todas formas es súper divertido... ¡así que hagámoslo!
Abre nuestro autentificador personalizado: src/Security/LoginFormAuthenticator.php
. Ya podemos ver que AbstractFormLoginAuthenticator
del antiguo sistema está obsoleto. Cámbialo por AbstractLoginFormAuthenticator
.
... lines 1 - 20 | |
use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator; | |
... lines 22 - 23 | |
class LoginFormAuthenticator extends AbstractLoginFormAuthenticator | |
{ | |
... lines 26 - 107 | |
} |
Lo sé, es casi el mismo nombre: sólo hemos intercambiado "Formulario" y "Inicio de sesión". Si tu autentificador personalizado no es para un formulario de inicio de sesión, entonces cambia tu clase aAbstractAuthenticator
.
Ah, y ya no necesitamos implementar PasswordAuthenticatedInterface
: eso era algo para el antiguo sistema.
El antiguo sistema de guardia y el nuevo sistema de autentificador hacen lo mismo: averiguan quién está intentando iniciar sesión, comprueban la contraseña y deciden qué hacer en caso de éxito o fracaso. Pero el nuevo estilo de autentificador se siente bastante diferente. Por ejemplo, puedes ver inmediatamente que PhpStorm está furioso porque ahora tenemos que implementar un nuevo método llamado authenticate()
.
Bien, bajaré a supports()
, iré a "Generar código" -o "cmd" + "N" en un Mac- e implementaré ese nuevo método authenticate()
. Este es el núcleo del nuevo sistema de autentificación... y vamos a hablar de él en unos minutos.
... lines 1 - 24 | |
class LoginFormAuthenticator extends AbstractLoginFormAuthenticator | |
{ | |
... lines 27 - 40 | |
public function authenticate(Request $request) | |
{ | |
// TODO: Implement authenticate() method. | |
} | |
... lines 45 - 113 | |
} |
Pero los sistemas antiguo y nuevo comparten varios métodos. Por ejemplo, ambos tienen un método llamado supports()
... pero el nuevo sistema tiene un tipo de retorno bool
. En cuanto añadimos eso, PhpStorm se alegra.
... lines 1 - 25 | |
class LoginFormAuthenticator extends AbstractLoginFormAuthenticator | |
{ | |
... lines 28 - 35 | |
public function supports(Request $request): bool | |
{ | |
... lines 38 - 39 | |
} | |
... lines 41 - 114 | |
} |
Abajo, en onAuthenticationSuccess()
, parece que también tenemos que añadir un tipo de retorno aquí. Al final, añade el tipo Response
de HttpFoundation. ¡Bien! Y mientras trabajamos en este método, cambia el nombre del argumento $providerKey
por$firewallName
.
... lines 1 - 25 | |
class LoginFormAuthenticator extends AbstractLoginFormAuthenticator | |
{ | |
... lines 28 - 90 | |
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): Response | |
{ | |
... lines 93 - 97 | |
} | |
... lines 99 - 114 | |
} |
No hace falta que lo hagas, simplemente es el nuevo nombre del argumento... y es más claro.
A continuación, abajo, en onAuthenticationFailure()
, añade allí también el tipo de retorno Response
. Ah, y para onAuthenticationSuccess()
, acabo de recordar que esto puede devolver un Response
anulable. En algunos sistemas -como la autenticación con token de la API- no devolverá una respuesta.
... lines 1 - 25 | |
class LoginFormAuthenticator extends AbstractLoginFormAuthenticator | |
{ | |
... lines 28 - 99 | |
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response | |
{ | |
... lines 102 - 108 | |
} | |
... lines 110 - 114 | |
} |
Por último, seguimos necesitando un método getLoginUrl()
, pero en el nuevo sistema, éste acepta un argumento Request $request
y devuelve un string
.
... lines 1 - 25 | |
class LoginFormAuthenticator extends AbstractLoginFormAuthenticator | |
{ | |
... lines 28 - 110 | |
protected function getLoginUrl(Request $request): string | |
{ | |
return $this->urlGenerator->generate(self::LOGIN_ROUTE); | |
} | |
} |
Muy bien, todavía tenemos que rellenar las "tripas", pero al menos tenemos todos los métodos que necesitamos.
Y de hecho, ¡podemos eliminar uno de ellos! Eliminar el método supports()
.
... lines 1 - 25 | |
class LoginFormAuthenticator extends AbstractLoginFormAuthenticator | |
{ | |
... lines 28 - 35 | |
public function supports(Request $request): bool | |
{ | |
return self::LOGIN_ROUTE === $request->attributes->get('_route') | |
&& $request->isMethod('POST'); | |
} | |
... lines 41 - 114 | |
} |
Vale, este método sigue siendo necesario para los autentificadores personalizados y su función es la misma que antes. Pero, si saltas a la clase base, en el nuevo sistema, el métodosupports()
está implementado para ti. Comprueba que la petición actual es un POST
y que la URL actual es la misma que la de inicio de sesión. Básicamente, dice
Apoyo a la autentificación de esta petición si se trata de una petición POST al formulario de inicio de sesión.
Antes escribimos nuestra lógica de forma un poco diferente, pero eso es exactamente lo que estábamos comprobando.
Bien, es hora de llegar a la carne de nuestro autentificador personalizado: el método authenticate()
. Hagámoslo a continuación.
Hey @MDelaCruzPeru
You can use the LoginFormAuthenticator
if you have a login form or create a custom authenticator to fill in the logic for fetching users through the API. If I recall correctly, on a successful login, you'll need to return an instance of UserInterface
, you can just add a User class to your project, it does not need to be an entity, just a holder for your users' info
You'll also need that UserProvider
because Symfony will use it to reload your user object. You can learn more about Symfony's security here https://symfonycasts.com/screencast/symfony-security - The tutorial it's built on Symfony 5 but all concepts are up to date
Cheers!
Hi,
I already had the authenticate method in Symfony 5, however after upgrading to 6 I get this error when a user is not found or types in an incorrect password:
Symfony\Component\HttpFoundation\Exception\ BadRequestException
Input value "login_form" contains a non-scalar value.
<?php
use App\Kernel;
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
return function (array $context) {
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
};
This is the code I use:
public function authenticate(Request $request): Passport
{
$email = $request->get('login_form')['_username'];
$password = $request->get('login_form')['_password'];
return new Passport(
new UserBadge($email, function($userIdentifier) {
// optionally pass a callback to load the User manually
$user = $this->em->getRepository(User::class)->findOneBy(['email' => $userIdentifier]);
if (!$user) throw new UserNotFoundException();
if (!$user->isEnabled()) throw new CustomUserMessageAuthenticationException('The account is deactivated.');
if ($user->isLocked()) throw new CustomUserMessageAuthenticationException('The account is blocked.');
return $user;
}),
new PasswordCredentials($password),
[
new CsrfTokenBadge(
'authenticate',
$request->request->get('_csrf_token')
),
(new RememberMeBadge())->enable(),
]
);
}
Any idea what might cause this? It seems to not like the Exceptions I defined in the UserBadge.
Hey Dirk,
Yeah, the get()
request method does not work with arrays in Symfony 6.x anymore, only with scalar values. You need to use all()
instead, e.g. in your case it should be something like this:
$parameters = $request->request->all();
$email = $parameters['login_form']['_username'];
// and so on...
See a related PR https://github.com/php-translation/symfony-bundle/pull/488 for more context.
Cheers!
Hello MolloKhan,
Thank you for the quick response.
We only use http_basic_ldap
.
It goes wrong when i want to start a session, when we have no PHP_AUTH_USER
.
If i turn of enable_authenticator_manager
everything works fine.
When I start a session and should get a popup for username, password, i get this error:
An exception has been thrown during the rendering of a template
("Cannot autowire argument $user of "App\Controller\<controller>": it references
interface "Symfony\Component\Security\Core\User\UserInterface" but no such service exists. Did you
create a class that implements this interface?").
This is our services.yaml
Symfony\Component\Ldap\Ldap:
arguments: [ '@Symfony\Component\Ldap\Adapter\ExtLdap\Adapter' ]
tags:
- ldap
Symfony\Component\Ldap\Adapter\ExtLdap\Adapter:
arguments:
- connection_string: 'ldap://%env(LDAP_URL)%:389'
options:
protocol_version: 3
referrals: false
And this is our security.yaml
security:
enable_authenticator_manager: true
providers:
my_ldap:
ldap:
service: Symfony\Component\Ldap\Ldap
base_dn: ...
search_dn: ...
search_password:
default_roles: ROLE_USER
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
stateless: true
provider: my_ldap
http_basic_ldap:
service: Symfony\Component\Ldap\Ldap
dn_string:...
Thanks for your help
Hey Annemieke, sorry for my slow response, but I was on holiday :)
You seem to be trying to inject a User object into a controller's action. Double-check the arguments of your controller
Hi MolloKhan,
Hope you had a nice vacation.
I get errors with every url (in the controller, voter etc.).
Because the username, password popup
does not show up while it should when it is a new session.
When i turn of enable_authenticator_manager
the popup does show, you fill in your username and password and everything works fine.
Can i send you some code or anything for you to try and replicate this situation?
Thanks in advance.
That's interesting... yea, I think I'd need to see your code. Perhaps you could give me access to your repository?
Hi Diego,
Do do you have some base example of a working ldap authentication program with symfony 6?
I have it working with symfony 5 and with symfony 3 for the last 4 years.
And what code do you need exactly?
Thanks in advance.
With kind regards,
Annemieke
Hey Annemieke-B,
Symfony Docs have some examples about authenticating against an LDAP server, you can check it here: https://symfony.com/doc/current/security/ldap.html - unfortunately, I have never used this before, so can't help with examples, but I hope the official docs help!
Cheers!
I'm afraid I do not have a working example. I'd like to play around with your application so I can debug what's going on, but I'd understand if you cannot share your app's code. I'll ask the team if someone can jump in to help
Hi all,
It goes wrong in HttpBasicAuthenticator class.
public function supports(Request $request): ?bool
{
return $request->headers->has('PHP_AUTH_USER');
}
Since with a new session i do not have a PHP_AUTH_USER
i will never get a response true.
For testing purposes I've added a few lines of code and everything works like a charm:
public function supports(Request $request): ?bool
{
if (!$request->headers->has('PHP_AUTH_USER')) {
$request->headers->set('PHP_AUTH_USER', '')
}
return $request->headers->has('PHP_AUTH_USER');
}
But now of course i have to find out how to set PHP_AUTH_USER
outside vendor code.
Hey Annemieke, an option would be to create a custom authenticator that extends from HttpBasicAuthenticator
so you can override the supports()
method and add those lines
Cheers!
Hi Diego, thanks for the help.
HttpBasicAuthenticator is a final class, so I cannot extend it.
Or did you mean something else?
Greetz!
ps. I think i have to do something in apache virtual hosts.
Ohh, I was not aware of that (personally, I don't like final classes too much :p). What you can do instead is to decorate the HttpBasicAuthenticator
class - https://symfony.com/doc/current/service_container/service_decoration.html
Cheers!
Hi Symfony Casts,
Again, great work people, thank you very much for making these videos.
Finally got my employers to invest in its developers and arrange a team account for symfony casts !!!
My question is:
We use ldap for authentication and authorization in symfony 5.4.
This works fine as long as we do not set 'enable_authenticator_manager' to true.
We want to use the LdapAuthenticator class of course, but the code first goes to HttpBasicAuthenticator
.
There the support function gives us a false because of course we do not use PHP_AUTH_USER
.
If i hack this function and let it always return true, it will get to LdapAuthenticator.
When i do a dump in the AuthenticatorManager class in the constructor of $this->authenticators
i get:
0 => Symfony ...\LdapAuthenticator {
authenticator: Symfony ..\HttpBasicAuthenticator { // is this HttpBasicAuth... correct??
userProvider: Symfo..LdapUserProvider
......
}
}
Thank you very much for your help. Hope to hear from you soon, we are upgrading to sf 6!
Kind regards,
Annemieke Buijs
Hey Annemieke,
Congratulations on convincing your employers! They're not going to regret this ;)
About your problem. When you activate the enable_authenticator_manager
, it enables the new Symfony security system, so you need to adapt your authenticators. Perhaps you already did it, so the next step is to set up the execution order of your custom authenticators by tweaking the security.firewalls.main.custom_authenticators
config option in your security.yaml
file. Symfony will call the authenticators from top to bottom and will stop calling them as soon as one of them returns true
from its supports
method
Cheers!
// composer.json
{
"require": {
"php": "^8.0.2",
"ext-ctype": "*",
"ext-iconv": "*",
"babdev/pagerfanta-bundle": "^3.6", // v3.6.1
"composer/package-versions-deprecated": "^1.11", // 1.11.99.5
"doctrine/annotations": "^1.13", // 1.13.2
"doctrine/dbal": "^3.3", // 3.3.5
"doctrine/doctrine-bundle": "^2.0", // 2.6.2
"doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
"doctrine/orm": "^2.0", // 2.11.2
"knplabs/knp-markdown-bundle": "^1.8", // 1.10.0
"knplabs/knp-time-bundle": "^1.18", // v1.18.0
"pagerfanta/doctrine-orm-adapter": "^3.6", // v3.6.1
"pagerfanta/twig": "^3.6", // v3.6.1
"sensio/framework-extra-bundle": "^6.0", // v6.2.6
"sentry/sentry-symfony": "^4.0", // 4.2.8
"stof/doctrine-extensions-bundle": "^1.5", // v1.7.0
"symfony/asset": "6.0.*", // v6.0.7
"symfony/console": "6.0.*", // v6.0.7
"symfony/dotenv": "6.0.*", // v6.0.5
"symfony/flex": "^2.1", // v2.1.7
"symfony/form": "6.0.*", // v6.0.7
"symfony/framework-bundle": "6.0.*", // v6.0.7
"symfony/mailer": "6.0.*", // v6.0.5
"symfony/monolog-bundle": "^3.0", // v3.7.1
"symfony/property-access": "6.0.*", // v6.0.7
"symfony/property-info": "6.0.*", // v6.0.7
"symfony/proxy-manager-bridge": "6.0.*", // v6.0.6
"symfony/routing": "6.0.*", // v6.0.5
"symfony/runtime": "6.0.*", // v6.0.7
"symfony/security-bundle": "6.0.*", // v6.0.5
"symfony/serializer": "6.0.*", // v6.0.7
"symfony/stopwatch": "6.0.*", // v6.0.5
"symfony/twig-bundle": "6.0.*", // v6.0.3
"symfony/ux-chartjs": "^2.0", // v2.1.0
"symfony/validator": "6.0.*", // v6.0.7
"symfony/webpack-encore-bundle": "^1.7", // v1.14.0
"symfony/yaml": "6.0.*", // v6.0.3
"symfonycasts/verify-email-bundle": "^1.7", // v1.10.0
"twig/extra-bundle": "^2.12|^3.0", // v3.3.8
"twig/string-extra": "^3.3", // v3.3.5
"twig/twig": "^2.12|^3.0" // v3.3.10
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.1
"phpunit/phpunit": "^9.5", // 9.5.20
"rector/rector": "^0.12.17", // 0.12.20
"symfony/debug-bundle": "6.0.*", // v6.0.3
"symfony/maker-bundle": "^1.15", // v1.38.0
"symfony/var-dumper": "6.0.*", // v6.0.6
"symfony/web-profiler-bundle": "6.0.*", // v6.0.6
"zenstruck/foundry": "^1.16" // v1.18.0
}
}
Hello everybody, I'm trying to implement security with no user table and authentication authorization all through an API, but I'm a little bit confused about what is the best way to implement it. Firstly I created the user with the make user method and Symfony creates the User class and a UserProvider class, then I check the course and learned about LoginFormAutheticator. I am already getting the response from the API with this piece of code in the class UserProvider implements UserProviderInterface, PasswordUpgraderInterface. But this method should return UserInterface and not a Passport. Do I have to implement the LoginFormAuthenticator instead of this method? What I'm missing is creating the user, the roles and processing the login. The User class created by Symfony implements UserInterface. Maybe is only an issue of filling the methods setUsername and setRoles and I can add there anything I need to store user information. Thanks for your help.