Este foro ya no está activo, así que no puedes publicar nuevas preguntas ni responder a las preguntas existentes.

Creando un proveedor de usuarios propio con Silex

1 de septiembre de 2014

Hola,

Estoy con el backend para una aplicación y como tenemos varios iguales se van a unificar y claro, la forma de identificar cada uno es con un código de cliente: un esquema común de base de datos con los usuarios y luego cada cliente con su esquema.

Total, que el login tiene que tener además de usuario y contraseña (obvio) un "código de cliente", ¿cómo puedo validar ese tercer parámetro?. En el UserProvider tengo el método loadUserByUsername() que recibe el nombre de usuario, típico, pero he intentado pasarle el Request o algo donde poder pillar los parámetros. El caso es que buscando he encontrado con un par de post similares por StackOverflow pero no termino de entender muy bien lo que hacer:

Estoy haciéndolo como el del primer enlace. Entiendo que tendría que añadir un campo _codigo, "codigo_parameter", ¿no?. ¿Cómo indico luego que use ese para validar? y lo mas importante, ¿cómo hago la consulta en loadUserByUsername(), porque solo recibe el nombre de usuario?.

Tengo muchas dudas la verdad, he trabajado con Silex para este y otros proyectos pero nunca me he metido tan a fondo.

Gracias de antemano.

Un saludo.


Respuestas

#1

Si te he entendido bien, lo que debes hacer es lo que se explica en el propio manual de Silex: Cómo utilizar un proveedor de usuarios propio en Silex.

Lo primero sería configurar tu propia clase proveedora de usuarios (debes definir la clase UserProvider en tu aplicación Silex) para poder pasarle el id de la empresa:

$app['security.firewalls'] = array(
    'admin' => array(
        'pattern' => '...',
        # ...
        'users' => $app->share(function () use ($app) {
            return new UserProvider($app['db'], $app['request']->get('company_id'));
        })
    )
);

Y después tendrías que definir el código de esa clase, que sería casi idéntico al que se muestra en el manual de Silex. Tú sólo tendrías que modificar el constructor de la clase y la consulta del método loadByUsername():

use Symfony\Component\Security\Core\User\UserProviderInterface;
use Doctrine\DBAL\Connection;
 
class UserProvider implements UserProviderInterface
{
    private $conn;
    private $companyId;
 
    public function __construct(Connection $conn, $companyId)
    {
        $this->conn = $conn;
        $this->companyId = $companyId;
    }
 
    public function loadUserByUsername($username)
    {
        $stmt = $this->conn->executeQuery('SELECT * FROM users WHERE username = ? AND company = ?', array(strtolower($username), (int) $companyId));
 
        if (!$user = $stmt->fetch()) {
            throw new UsernameNotFoundException(sprintf('Username "%s" does not exist.', $username));
        }
 
        // ...
    }
 
    // ...
}

@javiereguiluz

1 septiembre 2014, 23:24
#2

Muchas gracias, pero al hacerlo así me lanza el error:

RuntimeException: Accessed request service outside of request scope.
Try moving that call to a before handler or controller

El firewall lo tengo configurado en el archivo app.php, lo que hago es poner como zona segura /admin, pero realmente lo que hace el controlador es que cuando entras en / te redirige a /login y este a /admin. Vamos, que siempre te tienes que loguear. Este sería mi firewall:

$app['security.firewalls'] = array(
    'default' => array(
        'remember_me' => array(
            'lifetime' => 31536000,
            'path' => '/admin',
            'domain' => ''
        ),
        'pattern' => '^/admin',
        'anonymous' => true,
        'form' => array(
            'login_path' => '/login',
            'check_path' => '/admin/login_check',
            'default_target_path' => '/admin',
            'always_use_default_target_path' => true,
            'use_referer' => true
        ),
        'logout' => array(
            'logout_path' => '/admin/logout'
        ),
        'users' => $app->share(function () use ($app) {
            return new UserProvider($app['db'], $app['request']->get('ccode'));
        }),
    )
);

Lo demás lo he echo igual, al UserProvider le he añadido un atributo ccode (el código es texto en mi caso) y el resto es igual, variando la consulta. Lo que si he echo se añadir otro campo a mi clase User con el código para poder acceder a el fácilmente. El resto es igual pero no hay manera. Te pongo ambas clases:

UserProvider.php

<?php
 
namespace Users;
 
use Users\User;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Doctrine\DBAL\Connection;
 
class UserProvider implements UserProviderInterface {
    private $conn;
    private $ccode;
 
    public function __construct(Connection $conn, $ccode) {
        $this->conn = $conn;
        $this->ccode = $ccode;
    }
 
    public function loadUserByUsername($username) {
        // $sql = 'SELECT * FROM usuarios WHERE email_usuario = ?';
        $sql = "SELECT c.id, c.codigo, u.email_usuario, u.pass_usuario, u.roles
                FROM usuarios u
                INNER JOIN clientes c ON u.cliente_id = c.id
                WHERE LOWER(email_usuario) = ? AND LOWER(codigo) = ? AND c.estado = 1";
        $stmt = $this->conn->executeQuery($sql, array(strtolower($username), strtolower($ccode)));
 
        if (!$user = $stmt->fetch()) {
            throw new UsernameNotFoundException(sprintf('Username "%s" does not exist.', $username));
        }
 
        return new User($user['email_usuario'], $user['pass_usuario'], explode(',', $user['roles']), $user['codigo'], true, true, true, true);
    }
 
    public function refreshUser(UserInterface $user) {
        if (!$user instanceof User) {
            throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user)));
        }
 
        return $this->loadUserByUsername($user->getUsername());
    }
 
    public function supportsClass($class) {
        return $class === 'User';
    }
}

User.php

<?php
 
namespace Users;
 
use Symfony\Component\Security\Core\User\UserInterface;
 
class User implements UserInterface
{
    private $email_usuario;
    private $pass_usuario;
    private $roles;
    private $code;
 
    public function __construct($email_usuario, $pass_usuario, array $roles, $codigo) {
        $this->email_usuario = $email_usuario;
        $this->pass_usuario = $pass_usuario;
        $this->roles = $roles;
        $this->code = $codigo;
    }
 
    public function getRoles() {
        return $this->roles;
    }
 
    public function getPassword() {
        return $this->pass_usuario;
    }
 
    public function getSalt() {
    }
 
    public function getUsername() {
        return $this->email_usuario;
    }
 
    public function eraseCredentials(){
    }
 
    public function getCode() {
        return $this->code;
    }
 
    public function isEqualTo(UserInterface $user)
    {
        if (!$user instanceof User) {
            return false;
        }
 
        if ($this->pass_usuario !== $user->getPassword()) {
            return false;
        }
 
        if ($this->email_usuario !== $user->getUsername()) {
            return false;
        }
 
        return true;
    }
}

Según lo que he visto es que no puedo acceder a la petición en el middleware share, ¿pero como lo hago?. Voy a seguir probando.

Gracias.

Un saludo.

@LoGaNsF

2 septiembre 2014, 9:59
#3

El error que estás viendo se parece bastante a este issue reportado en el repositorio de Silex. Teóricamente ya está resuelto en este Pull Request. ¿La versión de Silex que utilizáis es más o menos moderna?

Otra posible solución que puedes explorar son los middlewares de Silex, que te permiten ejecutar código antes de la capa de seguridad utilizando el tipo de evento Application::EARLY_EVENT.

@javiereguiluz

2 septiembre 2014, 10:12
#4

La versión es la 1.2.1, no hace mucho que la actualice. He cambiado el código por:

...
'users' => $app->before(function () use ($app) {
    return new UserProvider($app['db'], $app['request']->get('ccode'));
}, Application::EARLY_EVENT)
...

Ahora no da error, solo que no me loguea aunque voy a mirar por si fuese de la base de datos ya que tengo varias conexiones (una por cada cliente), aunque entiendo que debería coger la primera que la he llamado default, ¿no?.

Gracias de nuevo, si no es correcto el código que he puesto indicamelo vaya ser que me este pegando cabezazos jeje :P

Un saludo.

@LoGaNsF

2 septiembre 2014, 10:33
#5

Me estoy volviendo loco. He puesto el código tal y como he puesto antes, no da error pero siempre me dice "Bad credentials" al loguear. Usando ladybug he visto que crea bien el usuario por lo que tanto la consulta como la clase User están correctas. ¿Porque me da siempre error en el login?. El caso es que si dejo el código como al principio, sin usar ese tercer parámetro, me loguea perfectamente.

Ya no se que hacer, he revisado y revisado la base de datos por si hubiese algo mal, pero es que esta bien.

EDIT: he vuelvo a usar share y he probado a pasar a pelo el código y funciona perfecto, pero necesito el código que escriba el usuario pero no puedo acceder al request. Sigo probando pero me tiene ya desesperado jeje. ¿No podría inyectar la petición de alguna forma?. Entiendo que ese servicio se ejecuta tanto cuando entro como cuando envío el login, ¿no?. Pero si no puedo acceder al request no le veo sentido a hacerlo de esta forma.

EDIT: voy avanzando pero no me muevo del sitio, actualmente tengo esto:

'users' => $app->share(function () use ($app) {
    return new UserProvider($app['db'], $app);
}),

Luego en el UserProvider accedo con $this->ccode['request']->get('ccode'), no me da error pero siempre me devuelve al login. Miro el log y obtengo lo siguiente:

[2014-09-02 16:27:26] myapp.INFO: Matched route "admin_login_check" (parameters: "_controller": "null", "_route": "admin_login_check") [] []
[2014-09-02 16:27:27] myapp.INFO: User "[email protected]" has been authenticated successfully [] []
[2014-09-02 16:27:27] myapp.DEBUG: Clearing remember-me cookie "REMEMBERME" [] []
[2014-09-02 16:27:27] myapp.DEBUG: Did not send remember-me cookie (remember-me parameter "_remember_me" was not sent). [] []
[2014-09-02 16:27:27] myapp.DEBUG: Remember-me was not requested. [] []
[2014-09-02 16:27:27] myapp.DEBUG: Notified event "kernel.request" to listener "Symfony\Component\HttpKernel\EventListener\ProfilerListener::onKernelRequest". [] []
[2014-09-02 16:27:27] myapp.DEBUG: Notified event "kernel.request" to listener "closure". [] []
[2014-09-02 16:27:27] myapp.DEBUG: Notified event "kernel.request" to listener "Silex\Provider\SessionServiceProvider::onEarlyKernelRequest". [] []
[2014-09-02 16:27:27] myapp.DEBUG: Notified event "kernel.request" to listener "Symfony\Component\HttpKernel\EventListener\RouterListener::onKernelRequest". [] []
[2014-09-02 16:27:27] myapp.DEBUG: Notified event "kernel.request" to listener "Silex\EventListener\LocaleListener::onKernelRequest". [] []
[2014-09-02 16:27:27] myapp.DEBUG: Notified event "kernel.request" to listener "Symfony\Component\Security\Http\Firewall::onKernelRequest". [] []
[2014-09-02 16:27:27] myapp.DEBUG: Listener "Symfony\Component\Security\Http\Firewall::onKernelRequest" stopped propagation of the event "kernel.request". [] []
[2014-09-02 16:27:27] myapp.DEBUG: Listener "closure" was not called for event "kernel.request". [] []
[2014-09-02 16:27:27] myapp.DEBUG: Listener "Silex\EventListener\LogListener::onKernelRequest" was not called for event "kernel.request". [] []
[2014-09-02 16:27:27] myapp.DEBUG: Listener "Silex\EventListener\MiddlewareListener::onKernelRequest" was not called for event "kernel.request". [] []
[2014-09-02 16:27:27] myapp.INFO: < 302 http://backend.bbgame/admin [] []
[2014-09-02 16:27:27] myapp.DEBUG: Write SecurityContext in the session [] []
[2014-09-02 16:27:27] myapp.DEBUG: Notified event "kernel.response" to listener "Silex\EventListener\MiddlewareListener::onKernelResponse". [] []
[2014-09-02 16:27:27] myapp.DEBUG: Notified event "kernel.response" to listener "Symfony\Component\HttpKernel\EventListener\ResponseListener::onKernelResponse". [] []
[2014-09-02 16:27:27] myapp.DEBUG: Notified event "kernel.response" to listener "closure". [] []
[2014-09-02 16:27:27] myapp.DEBUG: Notified event "kernel.response" to listener "Symfony\Component\Security\Http\RememberMe\ResponseListener::onKernelResponse". [] []
[2014-09-02 16:27:27] myapp.DEBUG: Notified event "kernel.response" to listener "Silex\EventListener\LogListener::onKernelResponse". [] []
[2014-09-02 16:27:27] myapp.DEBUG: Notified event "kernel.response" to listener "Symfony\Component\Security\Http\Firewall\ContextListener::onKernelResponse". [] []
[2014-09-02 16:27:27] myapp.DEBUG: Notified event "kernel.response" to listener "Symfony\Component\HttpKernel\EventListener\ProfilerListener::onKernelResponse". [] []
[2014-09-02 16:27:27] myapp.DEBUG: Notified event "kernel.response" to listener "Symfony\Bundle\WebProfilerBundle\EventListener\WebDebugToolbarListener::onKernelResponse". [] []
[2014-09-02 16:27:27] myapp.DEBUG: Notified event "kernel.finish_request" to listener "Symfony\Component\HttpKernel\EventListener\RouterListener::onKernelFinishRequest". [] []
[2014-09-02 16:27:27] myapp.DEBUG: Notified event "kernel.finish_request" to listener "Silex\EventListener\LocaleListener::onKernelFinishRequest". [] []
[2014-09-02 16:27:27] myapp.DEBUG: Notified event "kernel.finish_request" to listener "Symfony\Component\Security\Http\Firewall::onKernelFinishRequest". [] []
[2014-09-02 16:27:27] myapp.DEBUG: Notified event "kernel.terminate" to listener "closure". [] []
[2014-09-02 16:27:27] myapp.DEBUG: Notified event "kernel.terminate" to listener "Symfony\Component\HttpKernel\EventListener\ProfilerListener::onKernelTerminate". [] []
[2014-09-02 16:27:28] myapp.INFO: Matched route "admin" (parameters: "_controller": "{}", "_route": "admin") [] []
[2014-09-02 16:27:28] myapp.DEBUG: Read SecurityContext from the session [] []
[2014-09-02 16:27:28] myapp.DEBUG: Reloading user from user provider. [] []
[2014-09-02 16:27:29] myapp.WARNING: Username "" could not be found. [] []
[2014-09-02 16:27:29] myapp.INFO: Populated SecurityContext with an anonymous Token [] []
[2014-09-02 16:27:29] myapp.DEBUG: Access is denied (user is not fully authenticated) by "C:\xampp\htdocs\backend.bbgame\vendor\symfony\security\Symfony\Component\Security\Http\Firewall\AccessListener.php" at line 70; redirecting to authentication entry point [] []
[2014-09-02 16:27:29] myapp.DEBUG: Calling Authentication entry point [] []
[2014-09-02 16:27:29] myapp.DEBUG: Notified event "kernel.exception" to listener "Symfony\Component\HttpKernel\EventListener\ProfilerListener::onKernelException". [] []
[2014-09-02 16:27:29] myapp.DEBUG: Notified event "kernel.exception" to listener "Symfony\Component\Security\Http\Firewall\ExceptionListener::onKernelException". [] []
[2014-09-02 16:27:29] myapp.DEBUG: Listener "Symfony\Component\Security\Http\Firewall\ExceptionListener::onKernelException" stopped propagation of the event "kernel.exception". [] []
[2014-09-02 16:27:29] myapp.DEBUG: Listener "Silex\EventListener\LogListener::onKernelException" was not called for event "kernel.exception". [] []
[2014-09-02 16:27:29] myapp.DEBUG: Listener "Silex\ExceptionListenerWrapper::__invoke" was not called for event "kernel.exception". [] []
[2014-09-02 16:27:29] myapp.DEBUG: Listener "Silex\ExceptionListenerWrapper::__invoke" was not called for event "kernel.exception". [] []
[2014-09-02 16:27:29] myapp.DEBUG: Listener "Silex\ExceptionHandler::onSilexError" was not called for event "kernel.exception". [] []
[2014-09-02 16:27:29] myapp.INFO: < 302 http://backend.bbgame/login [] []

Parece que lo hace pero cuando me redirige a /admin no estoy logueado. ¿He echo algo mal en lo que he puesto? Qué locura, aunque estoy aprendiendo jeje.

@LoGaNsF

2 septiembre 2014, 12:21
#6

Siento postear tan seguido pero me voy a volver loco jeje. Me di por vencido ayer con el método que me comentabas, opté por pasar $app directamente al UserProvider y me ocurre lo que comentaba arriba, pero no hay manera.

Pues hoy he probado a crear mi propio proveedor de autenticación como en el manual y guiándome por la web de symfony y el segundo enlace que puse en mi primer post. Pero no hay manera. Mira que me he creado mi propia clase AbstractToken para crear el token de usuario añadiéndole un campo para el código, con un provider y un listener añadiendo el campo código junto con el username y password, pero nada.

Sigue yendo loadUserByUsername() aunque he creado y utilizado en el listener un método loadUserByUsernameCode que recibe usuario y código para sacar el usuario.

A ver si me podéis echar una mano que no puedo avanzar.

@LoGaNsF

3 septiembre 2014, 15:41
#7

Nunca he tenido que hacer un user provider con Silex, pero sigo pensando que la mejor manera de hacerlo sería seguir el método explicado en el manual oficial de Silex (ver respuesta anterior).

El problema era que en ese caso se te producía este error:

RuntimeException: Accessed request service outside of request scope.
Try moving that call to a before handler or controller

Para evitarlo, no pases el objeto Request mediante $app['request'], sino que puedes probar a pasar el objeto RequestStack. Te quedaría algo así:

$app['security.firewalls'] = array(
    'admin' => array(
        'pattern' => '...',
        # ...
        'users' => $app->share(function () use ($app) {
            return new UserProvider(
                $app['db'],
                $app['request_stack']->getCurrentRequest()->get('company_id')
            );
        })
    )
);

Lo del RequestStack no se explica en el manual de Silex, pero sí que lo explican en la documentación de Symfony.

@javiereguiluz

3 septiembre 2014, 22:08
#8

Muchas gracias, mañana lo probare en cuanto llegue, desconocía request_stack, a ver si de esa forma me sirve. De todas maneras actualmente lo tengo como me indicaste orignalmente, tenia que probar otros métodos jeje.

Lo único, tengo una clase User a la cual le he añadido un campo $ccode y le paso este también en el constructor, ¿no habrá problema por eso no?.

Gracias de nuevo.

@LoGaNsF

3 septiembre 2014, 22:40
#9

He estado probando lo que me comentabas y nada, ahora me lanza el error:

FatalErrorException: Error: Call to a member function get() on a non-object in...

Si pruebo a pasarle RequestStack directamente y obtener el código desde el UserProvider también peta. En el primer caso ni me muestra el login y en el segundo envío el formulario pero luego me da el mismo error. Bueno, no el mismo pero viene a ser lo mismo, que no puede acceder a ese valor. Siempre es null el getCurrentRequest().

¿Hay algo que me esté olvidando? Sólo he creado el UserProvider y mi clase User, nada de autenticación propia ni nada mas.

Gracias de nuevo.

@LoGaNsF

4 septiembre 2014, 10:14
#10

Como este no es un problema tan fácil de resolver, te propongo que hagamos una cosa. Primero vamos a hacer que funcione el login con tres campos poniendo a mano el código de la empresa. Luego ya veremos cómo obtener ese código y pasárselo al provider.

Así que te propongo que utilices el siguiente código con el id de empresa hardcodeado y compruebes si realmente los usuarios se pueden loguear en esa empresa:

$app['security.firewalls'] = array(
    'admin' => array(
        'pattern' => '...',
        # ...
        'users' => $app->share(function () use ($app) {
            return new UserProvider($app['db'], 'empresa_1');
        })
    )
);

@javiereguiluz

4 septiembre 2014, 10:24
#11

Ok, lo he puesto así y todo correcto, me loguea sin problemas. También he probado, que no lo puse, a comprobar que $app['request_stack']->getCurrentRequest() no era null y si era null meter el código a mano, si no, pues cogerlo del formulario pero siempre era null.

@LoGaNsF

4 septiembre 2014, 11:17
#12

¡Genial! Me alegro que esa parte funcione bien, ya que en teoría esa era la parte difícil. Lo que podemos hacer ahora para pasar el parámetro con el id de la empresa, es lo siguiente:

1. Creas un middleware de tipo Aplication::ERALY_EVENT, porque este es el único evento que se ejecuta antes de la capa de seguridad.

2. En ese middleware obtienes el id de la empresa y lo guardas como un parámetro de la aplicación. Ejemplo: $app['params.ccode'] = 'XXXX'

3. Si todo va bien, podrás reemplazar el valor hardcodeado del id de la empresa en tu proveedor por una llamada a $app['params.ccode'].

@javiereguiluz

4 septiembre 2014, 11:29
#13

Vamos avanzando, no me loguea pero porque el parámetro esta a null y me devuelve Bad credentials. Lo tengo así ahora mismo:

$app['security.firewalls'] = array(
    'default' => array(
        'pattern' => '^/admin',
        # ...
        'users' => $app->share(function () use ($app) {
            return new UserProvider($app['db'], $app['params.ccode']);
        }),
    )
);
 
$app['params.ccode'] = $app->before(function (Request $request) {
    return $request->get('_ccode');
}, Application::EARLY_EVENT);

Probando con ladybug compruebo que siempre entra en el before y toma bien el valor del formulario, pero si hago la comprobación en en share, cuando envío el formulario en $app['params.ccode'] es null siempre. Ahí me surge la duda, ¿cada vez que hago un intento de login entra en el share?. He probado a quitarlo y me lanza el error:

InvalidArgumentException: Identifier "params.ccode" is not defined.

@LoGaNsF

4 septiembre 2014, 12:23
#14

Creo que el middleware que se ejecuta antes que nada debería ser así:

$app->before(function (Request $request) use ($app) {
    $app['params.ccode'] = $request->get('_ccode');
}, Application::EARLY_EVENT);

@javiereguiluz

4 septiembre 2014, 12:33
#15

Inicialmente lo tenia así, pero me lanza el error anterior por lo que tenia que inicializar el parámetro antes aunque sea como una cadena vacía, pero claro, el error lo lanza la línea:

return new UserProvider($app['db'], $app['params.ccode']);

Está entrando antes que en el middleware, he probado incluso a ponerlo antes por si tenía algo que ver (todo esto está en el app.php), pero tampoco.

@LoGaNsF

4 septiembre 2014, 12:50
#16

He estado probando y pintando con print_r para ver por donde iba pasando y entra siempre en share antes que en el middleware before por mucho EARLY_EVENT que le ponga. Si lo pongo como indicaba en la respuesta #13 no tengo que inicializarla, pero no le pasa parámetro.

He estado trabajando con el código a pelo y esta todo correcto, pero sin login no me sirve de nada jeje.

@LoGaNsF

4 septiembre 2014, 19:59
#17

Aquí sigo dándole vueltas y en vistas de que ni avanzo ni se que coño puede ser, ¿habría la posibilidad de guardar el parámetro en sesión o como sea mediantes javascript?. Es lo que se me ocurre, capturar el evento submit del formulario, guardar el campo ccode de alguna forma y ya que siga su curso todo. Pero claro, imagino que pasaría antes por la capa de seguridad que por el javascipt (no lo se) y se jode el invento.

Otra opción seria loguear manualmente, ¿se puede?. Es decir, ¿podría hacerme un login y aun así mantener la seguridad?. Actualmente todo es para administradores, pero en cuestión de días también debe existir una parte para usuarios y el login va a ser el mismo. Si mantengo la seguridad me sirve, si no, descartado.

Como te salgas un poco de lo habitual vaya tela, debería ser mas fácil loguear con tres o mas parámetros, si al final la diferencia esta en la consulta pasa sacar el usuario, el único fijo es la contraseña. Claro que es mas fácil decirlo que hacerlo, pero se supone que los frameworks están para facilitar el trabajo entre otras muchas cosas.

@LoGaNsF

7 septiembre 2014, 19:46
#18

En efecto podrías optar a hacer el proceso de login a mano. Una vez hecha la consulta a la base de datos para comprobar si los datos del usuario son correctos, deberías crear un UsernamePasswordToken y guardarlo en el contexto de seguridad para loguear al usuario. Este proceso es exactamente igual que en Symfony, pero en este gist puedes encontrar un ejemplo de cómo hacerlo con Silex.

@javiereguiluz

7 septiembre 2014, 19:56
#19

¿El resto funciona todo igual, no?. Me refiero a que puedo definir a que zonas entran que usuarios y demás, ¿no?. Lo que no me queda claro del ejemplo que me has pasado es que el login_check va a /authenticate y abajo tiene creado /register, entiendo que debe ser un error y es a esa ruta a la que va y donde se hace el login, ¿no?.

Segun lo entiendo, en el controlador del login_check consulto a la base de datos para sacar el usuario (¿quito el 'users' del firewall?) y si está correcto genero el token, ¿lo he entendido bien?.

Muchas gracias de nuevo, mañana lo probare y te cuento. Lo saque o no te debo una bien grande aunque solo sea por aguantarme jeje :P

@LoGaNsF

7 septiembre 2014, 20:36
#20

El resto debería funcionar igual porque lo único que estás haciendo es pasar del sistema de login pero no del resto del componente de seguridad (por supuesto para asegurarte deberías crear al menos unos tests funcionales muy sencillos que comprueben que los usuarios anónimos no pueden acceder a las URL protegidas).

La idea sería redirigir el login a una acción tuya propia donde harás la consulta a la base de datos y si todo es correcto, creas el token a mano, lo guardas en el contexto de la seguridad de la aplicación y ya debería funcionar el resto sin problemas.

@javiereguiluz

7 septiembre 2014, 20:44
#21

¡Genio!, ahora si, he tenido que hacer alguna modificación porque he llegado a encontrar un hilo de ese mismo usuario indicando que no le funcionaba y a mi me pasaba lo mismo, que no guardaba el token y al hacer la redirección pues petaba. Al final mi acción ha quedado así:

$app->post('/authenticate', function(Request $request) use ($app) {
    $username   = $request->get('_username');
    $password   = $app['security.encoder.digest']->encodePassword($request->get('_password'), '');
    $ccode      = $request->get('_ccode');
 
    // Buscar usuario
    $sql = "...";
    $user = $app['db']->fetchAssoc($sql, array(strtolower($username), $password, strtolower($ccode)));
 
    // Loguear usuario
    if ($user) {
        $usuario = new User($user['email_usuario'], $user['pass_usuario'], explode(',', $user['roles']), $user['codigo']);
        $token = new UsernamePasswordToken($usuario, $usuario->getPassword(), 'default', $usuario->getRoles());
        $app['security']->setToken($token);
        $app['session']->set('_security_default', serialize($token));
    }
 
    return $app->redirect($app['url_generator']->generate('admin'));
})
->bind('custom_login');

He tenido que añadir la última línea dentro del if y ya funciona perfecto. Bueno, casi perfecto porque cuando los datos son erróneos no devuelve ningún error, pero es lo de menos.

Muchísimas gracias, espero poder devolverte la ayuda recibida algún día ;)

Un saludo.

@LoGaNsF

8 septiembre 2014, 10:59
#22

¡Me alegro que ya te funcione! Sobre el tema de que no muestra un mensaje de error en caso de que los datos sean incorrectos, podrías utilizar algo así:

use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
 
$app->post('/authenticate', function(Request $request) use ($app) {
    // ...
 
    // Loguear usuario
    if ($user) {
        // ...
 
        return $app->redirect($app['url_generator']->generate('admin'));
    }
 
    throw new AccessDeniedHttpException('El usuario y/o contraseña son incorrectos.');
})
->bind('custom_login');

@javiereguiluz

8 septiembre 2014, 11:09
#23

Ese error lo uso actualmente cuando el usuario no tiene permisos, cuando tiene ROLE_USER lanzo ese error y lo echo, aunque me lo apunto porque como ahora todos los usuarios se van a poder loguear pues lo controlo con eso. De momento le he puesto:

$app['session']->set('_security.last_error', 'Bad credentials.');

Quizás sea un poco chapuza pero es temporal.

@LoGaNsF

8 septiembre 2014, 11:27
#24

¿Pensabas que te ibas a librar de mi? jeje. Acabo de descubrir un problema. Resulta que si me logueo con usuario1/cliente1 o con usuario2/cliente2, sin problema, pero si se trata de usuario1/cliente3 (mismo usuario pero en otro cliente, con registro duplicado en la BBDD cambiando solo cliente_id), me pilla usuario1/cliente1. Haciendo pruebas he descubierto que entra en '/authenticate' pero después de crear el token entra en UserProvider y como ahí tengo la consulta que busca solo por usuario, pues me saca el primer usuario que coincide y me autentica con el. Te muestro como lo tengo:

app.php

$app['security.firewalls'] = array(
    'default' => array(
        'pattern' => '^/admin',
        'anonymous' => true,
        'form' => array(
            'login_path' => '/login',
            'check_path' => '/authenticate'
        ),
        'logout' => array(
            'logout_path' => '/admin/logout'
        ),
        'users' => $app->share(function () use ($app) {
            return new UserProvider($app['db']);
        }),
    )
);

controllers.php

$app->post('/authenticate', function(Request $request) use ($app) {
    $username   = $request->get('_username');
    $password   = $app['security.encoder.digest']->encodePassword($request->get('_password'), '');
    $ccode      = $request->get('_ccode');
 
    $app['session']->remove('_security.last_error');
 
    // Buscar usuario
    $sql = "SELECT c.id, c.codigo, u.email_usuario, u.pass_usuario, u.roles
            FROM usuarios u
            INNER JOIN clientes c ON u.cliente_id = c.id
            WHERE LOWER(email_usuario) = ? AND pass_usuario = ? AND LOWER(codigo) = ? AND c.estado = 1";
    $user = $app['db']->fetchAssoc($sql, array(strtolower($username), $password, strtolower($ccode)));
 
    // Loguear usuario
    if ($user) {
        $usuario = new User($user['email_usuario'], $user['pass_usuario'], explode(',', $user['roles']), $user['codigo']);
        $token = new UsernamePasswordToken($usuario, $usuario->getPassword(), 'default', $usuario->getRoles());
        $app['security']->setToken($token);
        $app['session']->set('_security_default', serialize($token));
    }
    else {
        $app['session']->set('_security.last_username', $username);
        $app['session']->set('_security.last_ccode', $ccode);
        $app['session']->set('_security.last_error', 'Bad credentials.');
    }
 
    return $app->redirect($app['url_generator']->generate('admin'));
})
->bind('custom_login');

En UserProvider.php hago la misma consulta pero sin el código. Depurando veo que el token que me genera en el action es correcto y si obtengo el código es el correcto, pero en cuanto hace la redirección genera otro token.

¿Alguna idea de como evitar que entre alli?. He probado a quitar 'users' del firewall pero obviamente no funciona.

Gracias.

EDIT: solucionado, resulta que entraba en el método refreshUser del UserProvider y este a su vez llama al loadUserByUsername, así que me he creado otro método que saca el usuario en función del nombre y código. Ahora va perfecto.

@LoGaNsF

9 septiembre 2014, 16:24