Proceso Login (Español)

Resumen

Liferay ofrece un complejo sistema de login que permite su personalización cubriendo prácticamente la totalidad de las necesidades que nos podamos encontrar. Para saber cómo personalizar correctamente el sistema es importante conocer a fondo este proceso de Login. Esta entrada (pese a no mostrar el detalle completo) pretende arrojar algo de luz sobre este tema.

Descripción a alto nivel

Por defecto el sistema de login de Liferay se basa en la validación de las credenciales enviadas por el usuario al sistema, utilizando la tabla User_ de la base datos de nuestro portal para ello.

En la configuración del portal podemos seleccionar cuáles serán los campos utilizables para dichas credenciales (por dirección de correo electrónico, por nombre de usuario o por identificador de usuario). En esta sección del panel de control también podemos configurar si queremos que se conecte con un LDAP para la autenticación y/o importación de usuarios, si queremos utilizar CAS, OpenSSO, NTLM o por URL, etc. Todas estas configuraciones tienen su explicación en el proceso de login de Liferay.

Para ilustrarlo vamos a ver un diagrama (ref: Wiki Liferay Authentation Process) con un resumen del proceso básico de login:

En este proceso podemos ver como cuando un usuario acceder al portal y le envía sus credenciales el portal llama a un método de la clase LoginAction. En dicho método llamará a otro método de la clase LoginUtil y así sucesivamente hasta completar el diagrama anterior. Más adelante profundizaremos en los métodos a los que se llama y su cometido, pues será de gran utilidad a la hora de modificar el comportamiento del sistema (mediante un Hook o, esperemos que no :P, un plugin-ext).

Lo que nos indica el diagrama anterior es, básicamente, que Liferay antes de realizar la autenticación de las credenciales del usuario contra los datos del mismo almacenados en la base de datos ejecutará una serie de métodos de las clases configuradas en la propiedad auth.pipeline.pre del portal.properties.

Estas clases tendrán que implementar com.liferay.portal.security.auth.Authenticator y por lo tanto sobrescribir los métodos authenticateByEmailAddress, authenticateByScreenName y authenticateByUserId. Estos métodos devolverán SUCCESS (1), FAILURE (-1) o DNE (0) en función del resultado de la autenticación de las credenciales del usuario para cada filtro. Es importante saber, que para que el login del usuario sea correcto es necesario que todos los filtros devuelvan SUCCESS, ya que si alguno devuelve algún error el proceso de login parará y no permitirá el acceso del usuario.

Una vez pasado todos los filtros se comprobará contra la base de datos si el usuario existe y si la contraseña es la correcta (esta comprobación está supeditada a una propiedad llamada auth.pipeline.enable.liferay.check la cual si es false el sistema no comprobará a esta altura si la contraseña coincide, sino que solo revisará si el usuario existe y es válido). Si las credenciales pasan esta comprobación, el sistema volverá llamar a los filtros, pero esta vez basándose en la propiedad auth.pipeline.post. Si todos los métodos correspondientes del este post-proceso también devuelve SUCCESS entonces el sistema devolverá una autenticación correcta, dando acceso al usuario al recurso solicitado.

Por defecto, solo hay un filtro en el auth.pipeline.pre, el del LDAP (LDAPAuth). Y no hay ningún filtro en el auth.pipeline.post. Pero añadiendo en estas propiedades nuestras propias clases (en un Hook, por ejemplo) podríamos modificar el tratamiento de las credenciales fácilmente.

El uso de la pipeline hace que sea relativamente fácil modificar el comportamiento del Login del portal. Pero este sistema se basa en tratar unos datos que el usuario le ha enviado al portal. Hay muchas otras situaciones en las que se necesita interferir en la forma en la que el usuario envía dicha información al portal, como por ejemplo con CAS, OpenSSO o NTLM. Para estos casos Liferay utiliza un sistema de AutoLogin.

Este sistema se basa en la propiedad auto.login.hooks la indica las clases que serán llamadas por orden para obtener las credenciales del usuario. Estas clases tienen que implementar com.liferay.portal.security.auth.AutoLogin y sobrescribir el método login. Dicho método recibirá todos los datos del request (y del response) y devolverá un array de String que representarán las credenciales. Estas credenciales se enviarán al punto de entrada del proceso básico de login mostrado anteriormente. Con lo que ya no será necesario que el usuario introduzca sus credenciales en el portlet de login del portal, sino que este se podrá integrar con otros sistemas para obtener dichas credenciales.

La ejecución secuencial de estas clases de AutoLogin se parará en el momento en el que alguna de las clases devuelva unas credenciales válidas. Por defecto la propiedad viene con todos los AutoLogin que trae de serie el portal, pero los métodos antes de ejecutarse revisan si estos filtros están activos en la configuración del portal. De esta manera si queremos activar un filtro de AutoLogin no es necesario que modifiquemos la propiedad auto.login.hooks, solo tendremos que activar el filtro en sí, bien en el panel de control bien en el portal-ext.properties.

Resumen descripción a alto nivel

El proceso de login comienza cuando un usuario intenta acceder a un recurso privado del portal o cuando introduce usuario/contraseña en el portlet de Login. En el caso de acceder a un recurso privado del portal, este ejecuta primero una series de filtros de AutoLogin buscando las credenciales del usuario (NTLM por ejemplo busca las credenciales que debe proporcionar el navegador a partir del usuario del dominio de sistemas basados en Windows). Si fallan todos los filtros de AutoLogin el usuario es redirigido al portlet de login (normalmente en /c/login) para que el usuario introduzca sus credenciales. Con las credenciales del usuario (ya sea por AutoLogin o introducidas por el usuario) se llama a los métodos de Login de Liferay los cuales primero ejecuta los filtros de pipeline. Si algún filtro falla (no devuelve SUCCESS) es señal de que el usuario no ha introducido las credenciales correctamente y el login fallaría. Una vez pasados los filtros de auth.pipeline.pre se autenticaría el usuario contra la base de datos y si esta no falla se pasarían los filtros auth.pipeline.post.

Integración con LDAP

Antes de continuar, entrando en detalle en el proceso de login, nos puede surgir la duda de cómo funciona Liferay con el LDAP. Para el caso del LDAP Liferay tiene un filtro auth.pipeline.pre llamado LDAPAuth, el cual loga el usuario utilizando contra el LDAP (o un AD o sistema similar) y si el resultado es SUCCESS entonces importa el usuario desde el LDAP a la base de datos de Liferay antes de que este compruebe sus credenciales contra la base de datos. De esta forma en cada acceso de los usuarios estos se importan o actualizan en la base de datos de Liferay.

Descripción detallada del proceso de Login

En el siguiente diagrama muestra en detalle el funcionamiento del sistema de Login de Liferay:

Para descargar la imagen

Lo primero que hace Liferay cuando recibe la petición de un usuario que no está logado y está intentando acceder a un recurso privado (documento, página, etc) es ejecutar todos los filtros de tipo AutoLogin. Estos filtros con clases java que implementan la clase AutoLogin. Por lo tanto tienen que sobrescribir el método login. Dicho método recibe como parámetros el request y el response de la petición y devuelve un array de String. Si el método obtiene las credenciales del usuario debe devolverlas en dicho array de String. Recordemos que las clases de autologin se ejecutan en el orden que se indique en la propiedad auto.login.hook. Por lo tanto si quisiéramos añadir un filtro más solo tendríamos que crear un hook añadiendo un nuevo elemento a la propiedad auto.login.hook y crear la clase que implemente com.liferay.portal.security.auth.AutoLogin.

Si alguno de estos filtros devuelve credenciales entonces el proceso continúa llamando al método processAction de la clase LoginAction. Si ninguno de los filtros devuelve credenciales entonces el usuario es redirigido a una página del portal donde pueda introducir sus credenciales en el portlet de login. Estas credenciales se guardarían en el request y también se llamaría al método processAction de la clase LoginAction.

El método processAction comprueba si la propiedad auth.login.disabled está deshabilitada. En caso de estar habilitada redirige al usuario a la página de login desactivado. En caso contrario continúa con el proceso de login llamando al método login de la misma clase LoginAction.

El método login obtiene las credenciales que debe estar almacenadas en la request que se le ha pasado por parámetros. Con dichas credenciales llamaría al método login de la clase LoginUtil. Dicho método es el que realiza el login del usuario propiamente dicho. Una vez se ejecute este método se hacen una serie de comprobaciones propias de JaaS y finaliza el proceso de login permitiendo el acceso al recurso solicitado por el usuario.

El método login de la clase LoginUtil obtiene el userId utilizando el método getAuthenticatedUserId de la misma clase. Una vez obtiene el userId realiza una serie de comprobaciones y genera la sesión del usuario (guarda sus datos en sesión y en las cookies para el usuario).

El método getAuthenticatedUserId chequea primero si se trata de una petición a /api/login o /api/secure/login (pues se trataría de un login automático desde la api). En caso afirmativo llamaría al método authenticateForBasic del servicio UserLocalServiceUtil repetidas veces (cambiando el tipo de login: AUTH_TYPE_EA, AUTH_TYPE_SN y AUTH_TYPE_ID) hasta que localiza el id del usuario (utilizando las credenciales suministradas). En caso negativo llama al método de autenticación correspondiente del servicio UserLocalServiceUtil en función del tipo de autenticación que tiene configurada la instancia del portal:

  • AUTH_TYPE_EA: llama a authenticateByEmailAddress
  • AUTH_TYPE_SN: llama a authenticateByScreenName
  • AUTH_TYPE_ID: llama a authenticateByUserId

Estos métodos deben devolver el resultado del login (SUCCESS, FAILURE o DNE), y solo en caso de devolver SUCCESS entonces el método getAuthenticatedUserId devuelve el id del usuario logado. En caso de no devolver SUCCESS entonces lanzaría una excepción.

Cualquier de estos métodos indicados anteriormente (authenticateBy…) del servicio UserLocalServiceUtil solo realizan una llamada al método authenticate de la misma clase pasándole por parámetro el tipo de autenticación y el screenName, emailAddress o userId en función del tipo.

El método authenticate tiene una mayor complejidad. Primero comprueba si ha recibido las credenciales (en caso contrario lanza una excepción que acaba capturando la clase LoginAction en el método processAction). Una vez comprobado que tenemos las credenciales del usuario llamaríamos al método authenticateByEmailAddress, authenticateByScreenName o authenticateByUserId de la interfaz AuthPipeline de pre auth, que básicamente llama al mismo método de cada clase que implemente Authenticator y que esté configurada en la propiedad auth.pipeline.pre. Como se ha indicado antes, para que el login continúe todas estas llamadas tienen que devolver SUCCESS, ya que del contrario contaría la ejecución y la llamada del método de la interfaz AuthPipeline devolvería el error correspondiente.

Una vez pasado el auth.pipeline.pre comprueba si el usuario que se está intentando logar existe y si no es Guest ni está bloqueado. Ya que en tal caso la función se pararía y devolvería el error correspondiente (DNE si el usuario no existe y FAILURE si el usuario está bloqueado o fuese Guest). En caso que el usuario exista, no esté bloqueado ni sea Guest la función comprueba si el auth.pipeline.pre ha devuelto SUCCESS y si la propiedad auth.pipeline.enable.liferay.check está a true. En tal caso comprueba si el usuario y la contraseña coinciden con el usuario de base de datos. En caso contrario se salta esta comprobación.

Por último, y solo si hasta ahora todas las comprobaciones han sido SUCCESS llama al post-procesado (igual que en auth.pripeline.pre) de las clases configuradas en la propiedad auth.pipeline.post.

Finalmente si el resultado de todas las operaciones ha sido SUCCESS se sale del método y finaliza el login en processAction (LoginAction). En caso contrario ejecuta todos los métodos onFailureByEmailAddress, onFailureByScreenName y onFailureByUserId de las clases configuradas en auth.pipeline.post.

Ejemplos de modificación del proceso de Login

Ahora veremos algunos ejemplos de modificaciones que se le podrían hacer al login:

  1. Añadir un nuevo tipo de login (por ejemplo SAML):
    • Para ello habría que añadir un nuevo tipo de AutoLogin que obtuviese las credenciales del usuario siguiendo el protocolo en cuestión y no sería necesario tocar el resto del proceso (en principio).
  2. Importar usuario directamente en el login (por ejemplo desde el LDAP):
    • Esto ya lo hace por defecto Liferay si activamos el LDAP. Liferay lo hace añadiendo un Authenticator en el auth.pipeline.pre que valida las credenciales del usuario contra el LDAP y en caso de ser correctas, obtiene los datos del usuario desde el LDAP y añade el usuario (o lo actualiza si ya existe) a la base de datos de Liferay, para que posteriormente en el chequeo de Liferay las credenciales del usuario estén actualizadas.
  3. Modificar el comportamiento del Login:
    • En principio nunca se debería modificar el comportamiento del login, pero si es necesario se podría crear un wrapper del servicio UserLocalService con un Hook para cambiar la lógica de los métodos authenticateBy… Pero no es buena idea. Es mucho mejor, deshabilitar el chequeo de las credenciales contra la base de datos (utilizando la propiedad auth.pipeline.enable.liferay.check y añadiendo un Authenticator que haga lo que necesitemos.