Un mecanismo sencillo y rápido de suplantación ( impersonation ) de identidad en Alfresco

En algunas ocasiones necesitamos suplantar (algunos lo traducen como impersonar, aunque la RAE no admite este “palabro”) la identidad de un usuario en Alfresco para llevar a cabo operaciones en su nombre. El objetivo de este post es explicar un mecanismo sencillo para poder enviar órdenes desde una aplicación Web a través de la API de WebServices de Alfresco hasta el repositorio, haciéndonos pasar por un usuario del que desconocemos su contraseña. En concreto, usaremos el usuario de Windows. 

 Nota: Aunque he tratado de hacer este artículo lo más didáctico y sencillo posible, es un artículo duro y necesitarás ciertos conocimientos y experiencia en el acceso a través de la API Web Services, autenticación NTLM, SOAP, Single Sign On y JSF para ser capaz de entenderlo y aplicarlo. ¡Tómate tu tiempo! 

Nota: El código de este post ha sido testeado sobre Alfresco Community 3.3.4. Para otras versiones puedes tener que hacer modificaciones leves. Si tienes problemas, ¡pon un comentario e intentaremos ayudarte!

Nuestro problema 

Podemos ver gráficamente qué es lo que nos proponemos

Suplantación ( impersonation ) Alfresco

 

Tal como muestra la figura, el escenario que queremos reproducir es el siguiente: 

 

Tenemos dos sistemas:

  1. Una aplicación web independiente (puede ser una aplicación Java, PHP, ASP.net o realizada con cualquier tecnología) que necesita interactuar con nuestro repositorio de gestión documental.
  2. Un repositorio basado en Alfresco que expone sus operaciones a través de una API de servicios Web.

 

La aplicación Web tiene conectividad con el repositorio de Alfresco a través del protocolo HTTP.

 

El escenario o caso de uso que queremos implementar es el que sigue:

 

  1. El usuario "amolina" solicita sobre una aplicación Web externa que se realice la operación "añadir un nuevo documento" a través de las operaciones que exponen los proxys del kit de desarrollo Alfresco SDK Remote. No se ha autenticado sobre la aplicación Web y por tanto esta desconoce sus credenciales ("usuario", "password").
  2. La aplicación Web externa solicita a Alfresco que añada el nuevo documento en nombre de "amolina". No se ha abierto explícitamente la sesión, es decir, NO se ha llamado a AuthenticationUtils.startSession(usuario, password) porque se desconoce el password del usuario (recordemos que este no se ha autenticado contra la aplicación Web y no tenemos ningún mecanismo para recuperar este password).
  3. Alfresco se cree que el usuario es "amolina" (se produce la suplantación aún cuando Alfresco no ha recibido las credenciales del usuario) y realiza la operación en su nombre.

¿Por qué necesitamos un mecanismo de suplantación de la identidad? 

Os podréis estar preguntando para qué necesitamos suplantar la identidad de un usuario, cuando lo normal es solicitarle a él el nombre de usuario y la contraseña y abrir la sesión en Alfresco a través del mecanismo estándar  AuthenticationUtils.startSession(usuario, password) o bien obteniendo el servicio de autenticación...

 AuthenticationServiceSoapBindingStub authenticationService = 
(AuthenticationServiceSoapBindingStub) new AuthenticationServiceLocator().getAuthenticationService();   
// Start the session   
 AuthenticationResult result = authenticationService.startSession(userName, password);

Pues bien, la razón más habitual es porque queremos implementar un mecanismo de Single SignOn. Es decir, queremos que el usuario no tenga que estar introduciendo sus credenciales constantemente cada vez que navegue a una nueva aplicación. O lo que es lo mismo, una vez que el usuario introduzca sus credenciales en algún sitio queremos propagar su identidad para que todas las aplicaciones lo reconozcan y no sea necesaria una nueva fase de autenticación.

Sabiendo esto, vamos a proponernos hacer un mecanismo de autenticación basado en el dominio de Windows o lo que es lo mismo, queremos que la aplicación Web solicite a Alfresco realizar las operaciones en nombre del usuario con el que hemos iniciado sesión en Windows y que este acepte dichas órdenes.

Paso 1: Single SignOn en Alfresco

Alfresco permite a partir de la versión 3.2 configurar distintos módulos de autenticación que implementan funcionalidades SingleSignOn (puedes ver los distintos módulos en Alfresco_Authentication_Subsystems y la información de versiones previas en SSO). Por ejemplo, en este post nos vamos a centrar en el módulo Passthru , que permite que cuando un usuario se autentica en un dominio de Windows sus credenciales se propaguen hasta Alfresco. O lo que es lo mismo, cuando un usuario entra en una máquina Windows que está integrada en un dominio de Windows e introduce sus credenciales (“nombre de usuario” y “contraseña”) este puede navegar directamente hasta Alfresco Explorer (http://<host>:<puerto>/alfresco)  y estará autenticado sin necesidad de volver a decir quién es.

Suplantación ( impersonation ) Alfresco - Windows Login - Alfresco Login

 Para conseguir esto, bastará con configurar el módulo passtrhu. Vamos a hacerlo, añadiendo el subsistema passtrhu al fichero alfresco-global.properties (versiones 3.2+) que está en el directorio de extensión <extensionRoot>. Para ello añadimos las siguientes claves de configuración: 

authentication.chain=passthru1:passthru
passthru.authentication.useLocalServer=false
passthru.authentication.domain=tekuento
passthru.authentication.servers=tekuento\\10.10.10.1,10.10.10.1
passthru.authentication.guestAccess=false
passthru.authentication.defaultAdministratorUserNames=amolina
#Timeout value when opening a session to an authentication server, in milliseconds
passthru.authentication.connectTimeout=5000
#Offline server check interval in seconds
passthru.authentication.offlineCheckInterval=300
passthru.authentication.protocolOrder=TCPIP
passthru.authentication.authenticateCIFS=true
passthru.authentication.authenticateFTP=true
ntlm.authentication.sso.enabled=true

Nótese que para que esta configuración funcione necesitaríamos tener un Active Directory  con nuestro dominio de Windows (“tekuento”) en la máquina 10.10.10.1.

Una vez hechos estos cambios podemos reiniciar Alfresco, navegar hasta Alfresco Explorer y comprobar cómo estaremos logueados en él con el usuario con el que hemos abierto sesión en Windows.

 

Paso 2 :  Single SignOn en nuestra aplicación Web independiente

Muy bien, ya hemos conseguido que Alfresco no solicite la autenticación cuando se utiliza Alfresco Explorer. Pero ¿qué pasa si le solicitamos una operación a través de la API de WebServices? Pues desgraciadamente vamos a obtener un error. Veámoslo.

 

Si ejecutamos un test del tipo, (¡¡recuerda que el TEST no debe abrir la sesión con startSession porque no conocemos las credenciales del usuario!!

 

 

public class TestConexionAlfrescoRemoto {

 public static void main(String[] args) { 
    try {
          // Si alfresco no esta en local, deberas decir cual es su IP
          // WebServiceFactory.setEndpointAddress("http://192.168.1.30:8080/alfresco/api");
          ClassificationServiceSoapBindingStub proxyServicioClasificacion=
          WebServiceFactory.getClassificationService();
         // Defino el espacio de almacenamiento donde trabajo
         Store store = new Store();
         store.setScheme(Constants.WORKSPACE_STORE);
         store.setAddress("SpacesStore");
         // Pido las clasificaciones 
                   Classification[] resultados =  
proxyServicioClasificacion.getClassifications(store);  
 
         for(int i=0; i<resultados.length; i++){
             Classification resultadoActual = resultados[i]; 

             System.out.println("Criterio de clasificacion :"  
+ 
             resultadoActual.getRootCategory().getTitle());          }
    } catch (AuthenticationFault e) {
       // TODO Auto-generated catch block
       System.out.println("Algo ha fallado..");
       e.printStackTrace();
    } catch (ClassificationFault e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    } catch (RemoteException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
     } 
 }

 

 

Obtendremos la excepción

 

Caused by: org.alfresco.webservice.util.WebServiceException: Ticket could not be found when calling callback handler.
 at org.alfresco.webservice.util.AuthenticationUtils.handle(AuthenticationUtils.java:171)
 at org.apache.ws.security.handler.WSHandler.performCallback(WSHandler.java:734)
 ... 16 more

 

¿Por qué? La razón es bien sencilla. El módulo passtrhu de autenticación que hemos configurado hace la autenticación vía NTLM sólo cuando las peticiones HTTP van dirigidas a URLs de Alfresco Explorer, aplicando para ello un filtro servlet. Es decir, si las peticiones HTTP llegan a través de mensajes SOAP, Alfresco no sabe quién somos y por tanto nos dirá que no existe una sesión válida.

 

Cuando Alfresco recibe una petición SOAP a través de la API de WebServices lo primero que va a hacer es mirar las cabeceras de la petición SOAP y buscar unas cabeceras de seguridad que deben contener un ticket de sesión válido (las cabeceras son estándares y se denominan cabeceras WS-Security ). ¿Cómo obtengo un ticket de sesión válido? La única forma es abrir una sesión en Alfresco. Pero.... ¿Puedo abrir la sesión en Alfresco con startSession? ¡NO porque no sé cuál es el usuario y password de Windows desde mi aplicación y no puedo recuperarlos de ninguna forma (podríamos recuperar el usuario si integráramos un módulo de autenticación vía NTLM pero nunca el password)!

 

Bien, veamos donde estamos... para que Alfresco ejecute órdenes que recibe a través de SOAP necesita que estas tengan un ticket de sesión válido pero para conseguirlo necesito unos datos que no tengo. ¿Cómo lo hago?

 

La idea feliz (poco ortodoxa, por otro lado) 

Bien, en este punto vamos a darle la vuelta a la tortilla. La idea consiste en hacer unas redirecciones, de tal manera que cuando el usuario entre en nuestra aplicación Web independiente la petición sea redirigida a Alfresco Explorer. Al hacer esto, el módulo passtrhu o cualquier otro que esté configurado y que tenga soporte SSO abrirá una sesión sin solicitarle las credenciales al usuario. ¡Ya tenemos la sesión de Alfresco!. ¿Cuál es el problema? Pues .... que en nuestra aplicación Web independiente no conocemos cuál es la sesión que se ha abierto y por tanto no podríamos añadirla en las cabeceras de las peticiones SOAP que generemos. ¿Cómo podemos solucionarlo? La única forma es que Alfresco Explorer nos informe de cuál es la sesión que se ha abierto, dándonos el ticket.

 

Para solucionar el problema vamos a añadir una nueva página JSF/JSP (podríamos hacerlo con un Servlet) en Alfresco Explorer. Esta página JSF/JSP actuará como pasarela de autenticación, es decir, si recibe una petición lo único que hará será redirigirnos nuevamente a nuestra aplicación Web independiente y ... ¡enviarnos el ticket de la sesión así como el nombre de usuario a través de la petición HTTP!

 

Para conseguirlo debemos hacer lo siguiente:

  1. Crear un bean manejado que obtenga el valor del ticket de sesión y el usuario.
  2. Crear una página JSF/JSP que nos redirija a nuestra aplicación Web independiente y que le pase los datos de la sesión y el usuario a través de la petición HTTP.

Para implementar el primer paso crearemos una clase Java que actúe de bean manejado

 

package es.tekuento.alfresco.auxiliar;
import org.alfresco.service.cmr.security.AuthenticationService; 
public class PasarelaAutenticacion  {

    private AuthenticationService servicioAutenticacion;
         public String getTicket() {
       return servicioAutenticacion.getCurrentTicket();
    }

    public String getUsuario(){
       return servicioAutenticacion.getCurrentUserName();
    }

    public AuthenticationService getServicioAutenticacion() {
        return servicioAutenticacion;
    }

    public void setServicioAutenticacion(AuthenticationService servicioAutenticacion) {
       this.servicioAutenticacion = servicioAutenticacion;
    }
}

 

 

En el fichero de configuración del controlador de JSF debemos añadir la declaración de dicho bean. Para ello modificaremos el fichero faces-config-custom.xml de la carpeta WEB-INF de la aplicación Web de Alfresco y añadiremos la declaración del bean así como la inyección del servicio de autenticación a través del mecanismo de inyección de dependencias.

 

<!DOCTYPE faces-config PUBLIC "-//Sun Microsystems, Inc.//DTD JavaServer Faces Config 1.1//EN"
                              "http://java.sun.com/dtd/web-facesconfig_1_1.dtd">
<faces-config>
    <managed-bean>
        <managed-bean-name>PasarelaAutenticacion</managed-bean-name>
     <managed-bean-class>es.iat.alfresco.auxiliar.PasarelaAutenticacion</managed-bean-class>
     <managed-bean-scope>session</managed-bean-scope>
     <managed-property>
         <property-name>servicioAutenticacion</property-name>
         <value>#{AuthenticationService}</value>
      </managed-property>
    </managed-bean>
 
 <!--- otras declaraciones que ya tuvieramos -->
</faces-config>

 

 

Una vez hecho esto, pasaremos a crear la página JSF/JSP que actuará como pasarela de autenticación. Esta enviará los datos a una URL de la aplicación Web independiente. Sea dicha URL https://192.168.1.10/AplicacionWebTeKuento/index.jsf (¡podría ser cualquier otra!), la página quedaría tal como sigue

 

<%@page import="javax.faces.context.FacesContext" %>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="/WEB-INF/alfresco.tld" prefix="a" %>
<%@ taglib uri="/WEB-INF/repo.tld" prefix="r" %>

<% response.sendRedirect("https://192.168.1.10/AplicacionWebTeKuento/index.jsf?TICKET=" + 
(FacesContext.getCurrentInstance().getApplication().createValueBinding("#{PasarelaAutenticacion.ticket}").
getValue(FacesContext.getCurrentInstance())) +   "&user=" + (FacesContext.getCurrentInstance().
getApplication().createValueBinding("#{PasarelaAutenticacion.usuario}").
getValue(FacesContext.getCurrentInstance()))) ; %> 

 

Fíjate que la URL a la que redireccionamos lleva el prefijo https, es decir, que usamos un canal seguro para transmitir estos datos. Si no es así, podemos tener problemas si el canal no es seguro (en una intranet controlada no habría demasiado problema) y que alguien nos robe el ticket de sesión (es decir, ¡que podría suplantar a nuestro usuario!). 

 

¡¡¡Muy bien!!! Pues ya hemos conseguido reenviar el ticket de sesión a nuestra aplicación. Ahora queda la última parte. ¿Cómo lo manejamos en ese otro lado?

 

Manejando el ticket en nuestra aplicación y añadiéndolo a las cabeceras SOAP de seguridad (WS-Security)

 

Por último debemos configurar nuestra aplicación Web para que cuando reciba el ticket  lo  guarde y lo reenvíe en todas las peticiones a través de las cabeceras WS-Security de SOAP. De esta forma Alfresco sabrá quién somos y no lanzará una excepción de tipo “org.alfresco.webservice.util.WebServiceException: Ticket could not be found when calling callback handler.”. Para hacerlo basta con recuperar de la petición los valores del ticket y el usuario y crear un objeto AuthenticationDetails que almacenará dichas credenciales. Podemos hacerlo por ejemplo desde la página JSF o desde un Servlet. Posteriormente registramos ese objeto en nuestros proxys de Alfresco SDK Remote y con esto hemos terminado.

 

String usuarioRecibido = request.getParameter(“user”).toString();
String ticketRecibido = request.getParameter(“TICKET”).toString();
AuthenticationDetails authenticationDetails = new AuthenticationDetails(usuarioRecibido, ticketRecibido, null);
AuthenticationUtils.setAuthenticationDetails(authenticationDetails);

 

A partir de este momento podemos lanzar las peticiones que queramos y alfresco no fallará. Hemos conseguido suplantar al usuario sin necesidad de hacer ninguna integración compleja, simplemente añadiendo una pasarela de autenticación basada en redirecciones HTTP que, aunque poco ortodoxa, es sencilla y funciona a la perfección.

 

Espero que os haya sido útil. ¡No dudéis en añadir vuestros comentarios e ideas!

Comentarios
URL de Trackback:

comments powered by Disqus