Buenas, estoy intentando realizar un logín de dos etapas.
Tengo una aplicación donde los usuarios pertenecen a compañías, y cuando un usuario se loguea, debo verificar si este se encuentra en más de una compañía, ya que de ser así, debo mostrar una página para que el usuario seleccione con cuál de las compañías a las que pertenece quiere acceder a la plataforma.
Esto debe ser así ya que estamos migrando una aplicación a Symfony y la lógica de dicha aplicación en este aspecto debe mantenerse.
Los usuarios tienen perfiles (roles) diferentes por cada compañía a la que pertenecen y también manejan premios por compañía. Por lo tanto, cuando accede al sistema mucha de la lógica interna depende de la compañía con la que el usuario se encuentra logueado.
Entonces recapitulando mis necesidades son las siguientes:
Login | varias compañias? ---- Si ---- Pantalla (Seleccion Compañia) | | No | | | Los roles salen de una ---------------| Relación UserCompanyProfiles | Termina de loguear (Listeners de login Success y demás)
He intentando creando un SimpleFormAuthenticator
, pero no logro implementar de forma limpia la redirección hacia la página de login o la de selección de compañía dependiendo del caso.
También intenté implementar un proveedor de autenticación con su respectivo EntryPoint
, lo cual me permitió hasta cierto punto redirigir a login o a select compañía en base a diferentes tokens que implementé (UsernamePasswordToken
y MultipleCompaniesToken
).
Por otro lado intenté logueando al usuario, y luego en una acción de un controlador se hace la selección de la compañía (Un listener kernel.request
manda a esta acción si el token es MultipleCompaniesToken), donde al hacer la selección se actualiza el token en TokenStorage, pero entonces no se ejecutan los eventos pertinentes al logueo del componente security.
Como ven se me ha hecho complejo este asunto, por lo que quería saber si han tenido este inconveniente y cómo lo han solucionado.
Desde ya muchas gracias!
Respuestas
Antes que nada, admito que nunca me ha tocado hacer un login en dos pasos con Symfony. Tu solución parece correcta, pero como tú mismo dices, en mi opinión es demasiado compleja. La pregunta que me gustaría hacerte es: ¿no sería suficiente con jugar con el evento security.interactive_login
después de la primera autenticación?
- El usuario introduce su usuario + contraseña y se autentica en el sistema.
- En el evento
security.interactive_login
haces una consulta para ver si el usuario está relacionado con más de una empresa. - Si el usuario sólo tiene una empresa, seguir adelante sin ejecutar el segundo paso del login.
- Si el usuario tiene varias empresas, mostrarlas en una lista en la pantalla del segundo paso del login.
Luego en la entidad que representa al usuario podrías tener una propiedad llamada empresaActiva
para saber en todo momento con qué empresa se ha relacionado el usuario.
@javiereguiluz
Hola Javier, muchas gracias por tu respuesta.
Luego de muchos intentos de implementar SecurityFactories
, autenticadores y demás, no dí con una solución limpia que permitiese lograr el cometido, sin embargo pude aprender bastante sobre el componente de seguridad :) jejeje.
Lo que he hecho es precisamente lo que has dicho, el login normal y corriente, y además implementé un SuccessHandler (que es el que permite establecer un response) para verificar las empresas, y si tiene varias, lo mando a la pagina de selección de empresa.
Por otro lado estoy implementando un listener para que verifique que el usuario en sesión tenga una empresa activa, y si no la tiene (y no está en la página de selección de empresas) lanzo una excepcion propia NotActiveCompanyFoundException
.
Al firewall de seguridad le he implementado un EntryPoint
que cuando hay una excepción de seguridad verifica si es del tipo NotActiveCompanyFoundException
y manda a la página de selección de empresas.
Así puedo controlar que los usuarios sin empresa activa accedan en la aplicación.
Por último tengo un listener que funciona parecido al SwitchUser
, esperando un parametro que contiene el id de una empresa, para poder establecer/actualizar la empresa activa del usuario.
Hasta este punto veo que voy a necesitar otros listeners, ya que los usuarios deben aceptar unos terminos y condiciones la primera ves que acceden con cierta empresa a la aplicación, por lo que al establecerse la compañia activa, debo aplicar la misma lógica expuesta anteriormente.
Saludos!
@manuel_j555
Leyendo tu respuesta me queda clara una cosa: podrías dar una charla espectacular contando con detalle este caso de uso y profundizando en las posibilidades del componente de seguridad de Symfony. Muchas gracias por compartirlo con nosotros.
@javiereguiluz
En tener un tiempo voy a intentar crear un post sobre la solución implementada, por los momentos estoy a horas de salir de vacaciones de la empresa, así que estaré alejado del desarrollo unos días :)
@manuel_j555
Hola,
Les comparto mi experiencia tratando de hacer un login en 2 pasos con symfony 3.
1 Cree un bundle que se extiende de FOSUser.
1.1 Luego sobreescribi la clase UserChecker en el nuevo bundle.
En el metodo checkPreAuth agregue estas lineas lara inicializa unas variables de session.
$session = $this->request->getMasterRequest()->getSession(); $session->set('user_id', null);
En el metodo checkPostAuth seteo unas variables que necesito para asignar una empresa.
$masterRequest = $this->request->getMasterRequest(); $request = $masterRequest->request; $session = $masterRequest->getSession(); $session->set('_security.last_user_id', $user->getId()); $session->set('_security.last_password', $request->get('_password')); $companyForm=$request->get('form'); if(!empty($companyForm['company_id'])) { if($user->hasCompanyId($companyForm['company_id'])){ $session->set('company_id', $companyForm['company_id']); }else{ $ex = new CompanyNotFoundException('El susuario no tiene asignado la Empresa.'); $ex->setUser($user); throw $ex; } }else{ $ex = new CompanyNotFoundException('Por favor ingrese la Empresa.'); $ex->setUser($user); throw $ex; }
1.2 La declaracion del servicio quedo asi.
app.user_checker:
class: AppBundle\?\?\UserChecker arguments: [ "@request_stack" ] scope: request public: true
1.3 En el nuevo bundle sobreescribi el loginAction
public function loginAction(Request $request) { /** @var $session \Symfony\Component\HttpFoundation\Session\Session */ $session = $request->getSession(); $view=null; $user_id=$session->get('_security.last_user_id'); $repo = $this->getDoctrine()->getRepository('AppSecurityBundle:User'); $user = $repo->findOneById($user_id); $lastPassword=null; if(!is_null($user)) { $form = $this->createFormBuilder(array()) ->add('company_id', 'entity', array( 'class' => 'AppBunble\?\Entity\Company', 'label' => 'Empresas', 'choices' => $user->getCompanys(), 'multiple' => false, 'expanded' => false, 'required' => false, )) ->getForm(); $data = $form->getData(); $view=$form->createView(); $lastPassword=$session->get('_security.last_password'); }
1.4 En el template de login.html.twing agregue lo siguiente.
{% if form_company %} {#{ form_label(form_company.company_id) }#} {{ form_widget(form_company.company_id, {'attr': {'class': 'form-control', 'placeholder':'Empresa'}})}} {% endif %}
Espero sirva de ayuda .
Saludos
@cesarluisl