Symfony. User Checker para validar si un usuario puede autenticarse
Cómo usar servicios del tipo UserChecker para añadir una capa extra de validación a los usuarios que quieren autenticarse
Muchas veces necesitamos realizar alguna serie de validaciones extra cuando queremos comprobar si un determinado usuario puede autenticarse en nuestra aplicación.
Para realizar estas comprobaciones Symfony nos provee por medio de su componente Security de la posibilidad de definir lo que se conocen como UserCheckers, de modo que podamos evitar que un usuario se autentique en la aplicación aún habiendo introducido sus credenciales.
¿Cuándo puede ser útil esto? Por ejemplo si queremos comprobar que un usuario ha validado su cuenta, tiene una suscripción activa en nuestro servicio o no ha sido baneado por un administrador.
¡Vamos a ver cómo funcionan!
Declarando un UserChecker
Declarar un UserChecker en Symfony es muy sencillo. Bastará con crear una clase que implemente la interfaz UserCheckerInterface
, la cual nos “obligará” a implementar 2 métodos:
checkPreAuth
, donde podremos realizar comprobaciones antes de que el usuario sea autenticado.checkPostAuth
, donde realizaremos las comprobaciones pertinentes una vez que el usuario ha sido autenticado correctamente por medio del sistema que estemos usando.
¿Cuál es la diferencia? Realmente muy poca, ya que por ejemplo el RememberMeAuthenticationProvider
de Symfony invoca estos dos métodos secuencialmente. Sin embargo, puede ser que determinadas comprobaciones “pesadas” queramos llevarlas a cabo una vez que el usuario ha sido autenticado en el sistema para evitarnos la demora en la respuesta en el caso de que la autenticación fuese inválida.
En esta respuesta de StackOverflow podéis leer más sobre este tema:
Sabiendo esto, veamos cómo podemos resolver el siguiente caso de uso: un usuario sólo puede autenticarse en nuestra aplicación si su cuenta está activada.
Para ello, crearemos en la carpeta src/Security
la siguiente clase:

Como veis, estoy llevando a cabo la comprobación en el método checkPreAuth
, el cual recibe como argumento un objeto que implementa la interfaz UserInterface
de modo que podamos llevar a cabo las comprobaciones oportunas, en este caso, si el usuario está activado.
Una vez creada la clase, nos dirigiremos a nuestro archivo config/packages/security.yaml
y en nuestro firewall encargado de gestionar el login de nuestra aplicación añadiremos lo siguiente:

De modo que ya automáticamente se realizará esa comprobación cada vez que un usuario desee autenticarse. Fácil, ¿verdad? Bueno aún podemos mejorarlo.
Encadenando varios UserCheckers
Imaginad ahora que no sólo tenemos que hacer una comprobación sino varias, alguna de ella con lógica más compleja que un simple “if”, por ejemplo, estas dos:
- El usuario tiene la cuenta activada.
- Una comprobación contra un servicio de terceros vía llamada API para ver si el usuario tiene una suscripción activa.
La propiedad user_checker
del “firewall” tiene una limitación: sólo podemos especificar un UserChecker
, por lo que lo primero que podríamos pensar es en tener un servicio gigante con toda esa lógica acoplada a él.
Para que quede más claro lo que quiero evitar que hagáis imaginad algo así:

Aprovechando que, si no hemos tocado la configuración por defecto que hay en el archivo services.yaml
, los UserChecker
son también servicios, podemos llegar a una solución mucho más limpia: separar cada comprobación en un servicio aparte e inyectar todos en el UserChecker
que asociaremos al “firewall” (recordad que sólo podíamos asociar uno).
1️⃣ Comprobación de que un usuario tiene la cuenta activada

2️⃣ Comprobación de que el usuario tiene una suscripción activa en un servicio de terceros

Ahora lo que haremos será inyectar estos dos servicios en un nuevo “UserChecker” al que denominaremos CombinedUserChecker
:

Y que será el que asociaremos al “firewall”:
security:
firewalls:
main:
...
user_checker: App\Security\CombinedUserChecker
📼 Bonus track
El proceso de asociar nuevos UserChecker
a nuestro servicio CombinedUserChecker
no es muy mantenible que digamos, ya que nos exige que cada vez que creemos uno nuevo lo añadamos al constructor.
Por suerte Symfony nos permite etiquetar nuestros servicios de modo que podamos “coger” todos los servicios con una determinada etiqueta e inyectarlos de forma automágica en otro servicio.
Lo primero que haremos será añadir lo siguiente en nuestro archivo config/services.yaml
:

Esto lo que hará es, que todos los servicios que implementen la interfaz AppUserCheckerInterface
serán etiquetados automáticamente en el “container” con la etiqueta app.user_checker
.
A continuación crearemos esa interfaz AppUserCheckerInterface
en nuestra carpeta src\Security
:

Y haremos que nuestros UserChecker
individuales (todos excepto CombinedUserChecker)
implementen esta interfaz, por ejemplo:

Ahora, añadiremos otra línea a nuestro archivo services.yaml
:

De este modo, todos los servicios etiquetados como app.user_checker
serán inyectados en el primer argumento del constructor de nuestro servicio CombinedUserChecker
:

Y ahora sí, ya tenemos todo funcionando y muy fácil de extender.
Gracias a Sergio Rebollo por ayudarme a mejorar el código de esta parte y que en su primera versión tenía un bug
Conclusiones
Este tipo de soluciones que ofrece Symfony es lo que más me gusta de este “framework” y lo que refleja la gran mejora que ha experimentado en los últimos años.
Cada vez es más fácil acoplarse a la lógica interna y, si conocemos detalles como las etiquetas, nos resultará mucho más fácil escribir código más reutilizable y mantenible.
¿Quieres recibir más artículos como este?
Si te ha gustado este artículo te animo a que te suscribas a la newsletter que envío cada domingo con publicaciones similares a esta y más contenido recomendado: 👇👇👇