Autenticación con FOSUserBundle y Propel en Symfony2

June 18, 2021

En esta oportunidad estaré comentando sobre el manejo de usuarios en Symfony2, características básicas de su sistema de seguridad y como implementar en menos de 15 minutos un sistema de manejo de usuarios eficiente, así mismo, proporcionaré tips a lo largo del blog para evitar gastar tiempo buscando en internet y podamos concentrarnos en nuestra aplicación. Asumimos que existe conocimiento básico de PHP y que se tiene una instalación básica de Symfony2 para probar el código proporcionado.

Una de las partes mas sensibles al desarrollar una aplicación web es el manejo de usuarios, ya que además que hay que tomar acciones serias en relación a seguridad, puede convertirse en una labor tediosa y repetitiva. Symfony2 dispone de un sistema de seguridad bastante robusto y completo, siendo una de las principales características del framework, pero por defecto, no dispone de herramientas que por ejemplo, nos brinde un formulario de login y demás acciones necesarias para la gestión de usuarios. Una de las implementaciones mas comunes de Symfony2 es usar Propel para manejar loo referente a base de datos, Propel es un ORM para PHP5 que permite usar un conjunto de objetos para la manipulación de base de datos, es uno de los Bundles más utilizados en Symfony2, y de hecho, fue parte del mismo años atrás.

Por suerte, los amigos de Friends Of Symfony crearon un conjunto de herramientas que se adaptan al componente de seguridad de Symfony2, habilitando una manera sencilla y potente para gestionar usuarios llamada FOSUserBunde, la cual puede trabajar de fábrica con propel de forma muy sencilla. FOSUserBundle por defecto ofrece una clase User.php para la gestión de usuarios con propel; veamos cómo realizar una configuración básica de estas herramientas: En nuestro config.yml colocamos las siguientes líneas:

[prism:php] providers: fos_userbundle: id: fos_user.user_provider.username

firewalls: main: pattern: ^/ form_login: provider: fos_userbundle csrf_provider: security.csrf.token_manager
logout: true anonymous: true access_control:

  • { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
  • { path: ^/register, role: IS_AUTHENTICATED_ANONYMOUSLY }
  • { path: ^/resetting, role: IS_AUTHENTICATED_ANONYMOUSLY }

[/prism:php]

En esta configuración creamos un nuevo proveedor de seguridad, indicando a symfony que en caso de ser llamado, utilice la clase definida por el servicio: fos_user.user_provider.username para validar y recargar usuarios de la base de datos. Además, indicamos que para las rutas de login, register y resseting (funcionalidades que nos brinda FosUserBundle), pueden ser accesadas sin necesidad de estar autenticados. Luego configuramos FosUserBundle para que utilice la clase de usuario de propel y utilice el firewall que creamos anteriormente en security.yml:

[prism:php] fos_user: db_driver: propel firewall_name: main user_class: FOS\UserBundle\Propel\User [/prism:php]

Por último, debemos de cargar las rutas de acceso a las funcionalidades en routing.yml:

[prism:php] fos_user: resource: "@FOSUserBundle/Resources/config/routing/all.xml" [/prism:php]

Con la configuración anterior cargamos todas las rutas que trae el bundle, pero si queremos, se pueden implementar rutas especificas. Ya tenemos todas las funcionalidades del bundle listas (requiere que se modifiquen los estilos de las mismas), pero ¿Qué pasa si tenemos que sobrescribir la clase de FOSUser para propel, e implementar nuevas funcionalidades o métodos? Bueno, en teoría sería sencillo, solo extendemos dicha clase creando una clase custom, añadimos nuestros métodos, y cambiamos la clase en config.yml de la siguiente manera:

[prism:php] fos_user: db_driver: propel firewall_name: main user_class: Acme\MyBundle\Model\User [/prism:php]

A pesar de que esta solución funciona, por detrás, suceden cosas que no deberían suceder, como por ejemplo, la clase que se trabaja a lo largo del proceso de autenticación es la clase de FOSUser y no nuestra clase custom, entonces, pueden presentarse errores como que en nuestra aplicación el login siempre falla incluso con usuarios registrados, corrompiendo todo lo referido a usuarios de nuestra aplicación. Este problema es un tanto de difícil de reconocer pero fácil de solucionar, es simple, reside en donde se carga el usuario para pasarlo al componente de seguridad de Symfony2: el servicio declarado en security.yml anteriormente.

[prism:php] public function refreshUser(SecurityUserInterface $user) { if (!$user instanceof User && !$user instanceof PropelUser) { throw new UnsupportedUserException(sprintf('Expected an instance of FOS\UserBundle\Model\User, but got "%s".', get_class($user))); }

if (!$this->supportsClass(get_class($user))) {
    throw new UnsupportedUserException(sprintf('Expected an instance of %s, but got "%s".', $this->userManager->getClass(), get_class($user)));
}

if (null === $reloadedUser = $this->userManager->findUserBy(array('id' => $user->getId()))) {
    throw new UsernameNotFoundException(sprintf('User with ID "%d" could not be reloaded.', $user->getId()));
}

return $reloadedUser;

} [/prism:php]

Lo que se puede presentar en esta situación es que la clase que le llega al user provider no es la que espera, que es específicamente la clase FOS\UserBundle\Propel\User la cual nunca le va a llegar pues hemos sobrescrito la clase para FOSUser con Acme\MyBundle\Model\User. Esto es un problema con multiples discusiones, y básicamente, el problema es porque la clase está escrita explícitamente en el user provider, por lo que la solución es crear un custom user provider que trabaje con nuestra clase personalizada.

Para solventar lo anterior, Lo primero es crear nuestro user provider que extienda del user provider que necesitamos modificar:

[prism:php] //Acme\MyBundle\Security\UserProvider class UserProvider extends use FOS\UserBundle\Security\UserProvider { /**

  • @var UserManagerInterface */ protected $userManager;

    /**

  • Constructor.
  • @param UserManagerInterface $userManager */ public function construct(UserManagerInterface $userManager) { $this->userManager = $userManager; parent::construct($userManager); }

    public function refreshUser(UserInterface $user) { if (!$user instanceof User && !$this->supportsClass(get_class($user))) { throw new UnsupportedUserException(sprintf('Expected an instance of FOS\UserBundle\Model\User, but got "%s".', get_class($user))); }

    if (null === $reloadedUser = $this->userManager->findUserBy(array('id' => $user->getId()))) {
        throw new UsernameNotFoundException(sprintf('User with ID "%d" could not be reloaded.', $user->getId()));
    }
    return $reloadedUser;

    } } [/prism:php]

Luego lo declaramos como un servicio en el services.yml de nuestro Bundle:

[prism:php] //Acme\MyBundle\Resources\config\services.yml services: my.user.provider: class: Rootstack\Rootnet\UserBundle\Security\UserProvider arguments:

  • @fos_user.user_manager.default [/prism:php]

Y por ultimo, le decimos a FOSUserBundle que utilice nuestro nuevo user provider en security.yml:

[prism:php] providers: fos_userbundle: id: my.user.provider [/prism:php]

Ahora, nuestro problema es que a pesar que el login y demás funcionalidades trabajan correctamente, se sigue pasando la clase incorrecta en la aplicación, ahora nuestro problema es un problema de mapeo. Por alguna razón, se pasa la clase de usuario que extendimos en lugar de la nuestra, y por desgracia, no existe manera de solventarlo sin la ayuda de una herramienta adicional que realice el mapeo de nuestra clase correctamente. GlorpenPropelBundle permite extender las funcionalidades de propel añadiendo eventos adicionales, y demás funcionalidades destacando la que necesitamos para solventar nuestro problema de mapeo. La instalación es sencilla:

[prism:php] composer require glorpen/propel-bundle [/prism:php]

Luego añadimos las siguientes líneas en config.yml específicamente en la sección e propel:

[prism:php] build_properties: propel.behavior.extend.class: 'vendor.glorpen.propel-bundle.Glorpen.Propel.PropelBundle.Behaviors.ExtendBehavior' propel.behavior.default: "extend" [/prism:php]

Y por último:

[prism:php] glorpen_propel: extended_models: FOS\UserBundle\Propel\User: Acme\MyBundle\Model\User [/prism:php]

Ahora nuestra aplicación trabaja como debería de funcionar, con nuestra clase de usuarios personalizada, evitándonos futuros errores súper extraños en el proceso de autenticación. FOSUserBundle es casi una extensión de symfony2, y como pudimos observar, es super sencillo de implementar, además, con propel es increíblemente rápido de configurar ya que todas las cosas las trae por defecto, solo indicamos como queríamos usarlo y listo, no nos tenemos que preocupar por la lógica de autenticación y gestión de usuarios y nos dedicamos a la la lógica de la aplicación. Espero el mini tutorial le haya servido, y encuentren en FOSUserBundle la solución que buscaban para el manejo de usuarios, próximamente estaré escribiendo sobre el manejo de sesiones con JWT y FOSUserBundle en Symfony2.