gstreamer0.10-ffmpeg
gstreamer0.10-plugins-good
packages.
A nivel básico, autenticar a un usuario cuando enviamos el formulario de acceso es... bastante sencillo. Tenemos que leer el email
enviado, consultar la base de datos para ese objetoUser
... y finalmente comprobar la contraseña del usuario.
Lo raro del sistema de seguridad de Symfony es que... no vamos a escribir esta lógica en el controlador. No. Cuando hagamos un POST a /login
, nuestro autentificador va a interceptar esa petición y hará todo el trabajo por sí mismo. Sí, cuando enviemos el formulario de inicio de sesión, nuestro controlador en realidad nunca se ejecutará.
Ahora que nuestro autentificador está activado, al inicio de cada petición, Symfony llamará al método supports()
de nuestra clase. Nuestro trabajo es devolver true
si esta petición "contiene información de autenticación que sabemos procesar". Si no, devolvemos false
. Si devolvemos false
, no fallamos en la autenticación: sólo significa que nuestro autenticador no sabe cómo autenticar esta petición... y la petición continúa procesándose con normalidad... ejecutando cualquier controlador con el que coincida.
Así que pensemos: ¿cuándo queremos que nuestro autenticador "haga su trabajo"? ¿Qué peticiones "contienen información de autenticación que sabemos procesar"? La respuesta es: siempre que el usuario envíe el formulario de inicio de sesión.
Dentro de supports()
devuelve true si $request->getPathInfo()
-es un método elegante para obtener la URL actual- es igual a /login
y si $request->isMethod('POST')
:
... lines 1 - 11 | |
class LoginFormAuthenticator extends AbstractAuthenticator | |
{ | |
public function supports(Request $request): ?bool | |
{ | |
return ($request->getPathInfo() === '/login' && $request->isMethod('POST')); | |
} | |
... lines 18 - 43 | |
} |
Así que si la petición actual es un POST a /login
, queremos intentar autentificar al usuario. Si no, queremos permitir que la petición continúe de forma normal.
Para ver lo que ocurre a continuación, baja en authenticate()
, dd('authenticate')
:
Tip
PassportInterface
está obsoleto desde Symfony 5.4: utiliza en su lugar Passport
como tipo de retorno.
... lines 1 - 11 | |
class LoginFormAuthenticator extends AbstractAuthenticator | |
{ | |
... lines 14 - 18 | |
public function authenticate(Request $request): PassportInterface | |
{ | |
dd('authenticate!'); | |
} | |
... lines 23 - 43 | |
} |
¡Hora de probar! Ve a actualizar la página de inicio. ¡Sí! El método supports()
devolvíafalse
... y la página seguía cargándose con normalidad. En la barra de herramientas de depuración de la web, tenemos un nuevo icono de seguridad que dice "Autenticado: no". Pero ahora ve al formulario de inicio de sesión. Esta página sigue cargándose con normalidad. Introduce abraca_admin@example.com
-que es el correo electrónico de un usuario real de la base de datos- y una contraseña cualquiera -yo utilizaré foobar
-. Envíalo y... ¡lo tienes! ¡Ha llegado a nuestro dd('authenticate')
!
Así que si supports()
devuelve true, Symfony llama a authenticate()
. Este es el corazón de nuestro autentificador... y su trabajo es comunicar dos cosas importantes. En primer lugar, quién es el usuario que está intentando iniciar sesión -en concreto, qué objeto deUser
es- y, en segundo lugar, alguna prueba de que es ese usuario. En el caso de un formulario de acceso, eso sería una contraseña. Como nuestros usuarios aún no tienen contraseña, la falsificaremos temporalmente.
Comunicamos estas dos cosas devolviendo un objeto Passport
: return newPassport()
:
... lines 1 - 12 | |
use Symfony\Component\Security\Http\Authenticator\Passport\Passport; | |
... lines 14 - 15 | |
class LoginFormAuthenticator extends AbstractAuthenticator | |
{ | |
... lines 18 - 22 | |
public function authenticate(Request $request): PassportInterface | |
{ | |
... lines 25 - 27 | |
return new Passport( | |
... lines 29 - 32 | |
); | |
} | |
... lines 35 - 55 | |
} |
Este simple objeto es básicamente un contenedor de cosas llamadas "insignias"... donde una insignia es un pequeño trozo de información que va en el pasaporte. Las dos insignias más importantes son UserBadge
y una especie de "insignia de credenciales" que ayuda a demostrar que este usuario es quien dice ser.
Empieza por coger el correo electrónico y la contraseña que te han enviado:$email = $request->request->get('email')
. Si no lo has visto antes,$request->request->get()
es la forma de leer los datos de POST
en Symfony. En la plantilla de inicio de sesión, el nombre del campo es email
... así que leemos el campo POST email
. Copia y pega esta línea para crear una variable $password
que lea el campopassword
del formulario:
... lines 1 - 15 | |
class LoginFormAuthenticator extends AbstractAuthenticator | |
{ | |
... lines 18 - 22 | |
public function authenticate(Request $request): PassportInterface | |
{ | |
$email = $request->request->get('email'); | |
$password = $request->request->get('password'); | |
return new Passport( | |
... lines 29 - 32 | |
); | |
} | |
... lines 35 - 55 | |
} |
A continuación, dentro del Passport
, el primer argumento es siempre el UserBadge
. Dinew UserBadge()
y pásale nuestro "identificador de usuario". Para nosotros, ese es el $email
:
... lines 1 - 10 | |
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; | |
... lines 12 - 15 | |
class LoginFormAuthenticator extends AbstractAuthenticator | |
{ | |
... lines 18 - 22 | |
public function authenticate(Request $request): PassportInterface | |
{ | |
$email = $request->request->get('email'); | |
$password = $request->request->get('password'); | |
return new Passport( | |
new UserBadge($email), | |
... lines 30 - 32 | |
); | |
} | |
... lines 35 - 55 | |
} |
Hablaremos muy pronto de cómo se utiliza esto.
El segundo argumento de Passport
es una especie de "credencial". Eventualmente le pasaremos un PasswordCredentials()
.... pero como nuestros usuarios aún no tienen contraseñas, utiliza un nuevo CustomCredentials()
. Pásale una devolución de llamada con un argumento $credentials
y un argumento $user
de tipo-indicado con nuestra clase User
:
... lines 1 - 11 | |
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\CustomCredentials; | |
... lines 13 - 15 | |
class LoginFormAuthenticator extends AbstractAuthenticator | |
{ | |
... lines 18 - 22 | |
public function authenticate(Request $request): PassportInterface | |
{ | |
... lines 25 - 27 | |
return new Passport( | |
new UserBadge($email), | |
new CustomCredentials(function($credentials, User $user) { | |
... lines 31 - 32 | |
); | |
} | |
... lines 35 - 55 | |
} |
Symfony ejecutará nuestra llamada de retorno y nos permitirá "comprobar las credenciales" de este usuario de forma manual... sea lo que sea que eso signifique en nuestra aplicación. Para empezar, dd($credentials, $user)
. Ah, y CustomCredentials
necesita un segundo argumento, que es cualquiera de nuestras "credenciales". Para nosotros, eso es $password
:
... lines 1 - 15 | |
class LoginFormAuthenticator extends AbstractAuthenticator | |
{ | |
... lines 18 - 22 | |
public function authenticate(Request $request): PassportInterface | |
{ | |
... lines 25 - 27 | |
return new Passport( | |
new UserBadge($email), | |
new CustomCredentials(function($credentials, User $user) { | |
dd($credentials, $user); | |
}, $password) | |
); | |
} | |
... lines 35 - 55 | |
} |
Si esto de CustomCredentials
es un poco confuso, no te preocupes: realmente tenemos que ver esto en acción.
Pero en un nivel alto... es algo genial. Devolvemos un objeto Passport
, que dice quién es el usuario -identificado por su email
- y una especie de "proceso de credenciales" que probará que el usuario es quien dice ser.
Bien: con sólo esto, vamos a probarlo. Vuelve al formulario de acceso y vuelve a enviarlo. Recuerda: hemos rellenado el formulario con una dirección de correo electrónico que sí existe en nuestra base de datos.
Y... ¡impresionante! foobar
es lo que envié para mi contraseña y también está volcando el objeto de entidad User
correcto de la base de datos! Así que... ¡oh! De alguna manera, supo consultar el objeto User
utilizando ese correo electrónico. ¿Cómo funciona eso?
La respuesta es el proveedor de usuarios Vamos a sumergirnos en eso a continuación, para saber cómo podemos hacer una consulta personalizada para nuestro usuario y terminar el proceso de autenticación.
Wooow, I solved! This happened because I didn't type an email that exist on DB when login! That's crazy! I let the question here perhaps helps someone!
Hey zahariastefan462
That's so awesome that you solved it by yourself! Keep learning and stay in touch!
Cheers!
Hello,
in the callback function I type-hinted the argument $user to Symfony\Component\Security\Core\User\User
instead of using the entity : App\Entity\User
as mentioned in the tutorial, so I had this error Argument 2 passed to App\Security\LoginFormAuthenticator::App\Security\{closure}() must be an instance of Symfony\Component\Security\Core\User\User, instance of App\Entity\User given, called in
In case someone is having same error, just add use App\Entity\User;
instead of the one from Symfony\Component\Security\Core\User
Hey Henane,
Probably you mean Symfony\Component\Security\Core\User\UserInterface that your User entity should implement? Otherwise, make sure your user class extends that "Symfony\Component\Security\Core\User\User" core class if you want to typehint with it :)
Cheers!
Hello,
My User Entity already implements UserInterface, I am talking about the $user argument that we pass to callable of CustomCredentials, with autocompletion I had Symfony\Component\Security\Core\User\User
instead of App\Entity\User
which result to error mentioned in my previous comment.
Hey Hanane,
Yes, I understand! :) Just try to re-read my last comment one more time - you either should change your type hint to that "UserInterface" instead of that "Symfony\Component\Security\Core\User\User" class, or you should start extending that "Symfony\Component\Security\Core\User\User" in your User entity - that's how PHP works :) You just can't typehint method arguments with a class or interface and then pass an object that does not extends that class or implements that interface - that won't work, even if your class will have common method names with those classes/interfaces.
Or, it might be so that in the parent method there's already a tyehint with "App\Entity\User" and so you can't downgrade it to a more base class like "Symfony\Component\Security\Core\User\User". But that's only the case if your App\Entity\User class really extends that "Symfony\Component\Security\Core\User\User".
Cheers!
By the time you upgrade to a newer system, it's already deprecated again :(( I see PassportInterface is deprecated in 5.4 lol
Edit: it seems the only change is that authenticate() method should return Passport instead of PassportInterface
Hey The_nuts!
Whoops, that evolves very quickly :) Good catch btw! Yes, you're right, you should use Password instead of PasswordInterface, here's the link to the deprecation message for the reference: https://github.com/symfony/...
We will add a note in this tutorial. Thanks!
Cheers!
Hey wLcDesigns,
Do you have any specific use case? The main job of the authenticator is to say either the current user is authenticated or no, sanitizing might be an overkill, because if the data is corrupted in some way - authenticator will fail anyway.
Cheers!
// 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
}
}
Hello, after last step,
return new Passport(
new UserBadge($email),
new CustomCredentials(function($credentials, User $user) {
dd($credentials, $user);
}, $password)
);
I got this error when type Log In
Return value of App\Security\LoginFormAuthenticator::onAuthenticationFailure() must be an instance of Symfony\Component\HttpFoundation\Response or null, none returned
Seems that doesn't enter in new CustomCredentials function and I don't know why, is going directly to onAuthenticationFailure().