sábado, 22 de marzo de 2014

WSO2 ESB: Transformando de JSON a SOAP y de SOAP a JSON.



En esta entrada quiero mostrarles una de las maneras de implementar el siguiente escenario:

Se tiene un servicio backend implementado usando SOAP que recibe un valor de temperatura en Celsius y la devuelve en Fahrenheit, como puede ser el siguiente: http://www.w3schools.com/webservices/tempconvert.asmx?wsdl del cual nos interesa la operación CelsiusToFahrenheit.

Se desea que la petición del cliente sea un json y la respuesta igual sea un json.

La solución usando el WSO2 ESB es implementar un servicio proxy que reciba un json, lo transforme a un mensaje SOAP y lo mande al servicio backend, luego la respuesta SOAP la transforme nuevamente en un json y la mande de vuelta al cliente.

El mediador que usaremos para poder cambiar de un formato de mensaje a otro será el payloadfactory y verán lo útil que es a partir de las últimas versiones del WSO2 ESB, ya que nos permite tanto generar mensajes SOAP como JSON con bastante facilidad.

Para este servicio backend y usando el mediador PayloadFactory el servicio proxy queda como sigue:

<?xml version="1.0" encoding="UTF-8"?>
<proxy xmlns="http://ws.apache.org/ns/synapse"
       name="JsonToXMLProxy"
       transports="https http"
       startOnLoad="true"
       trace="disable">
   <description/>
   <target>
      <endpoint>
         <address uri="http://www.w3schools.com/webservices/tempconvert.asmx" format="soap11"/>
      </endpoint>
      <inSequence>
         <log>
            <property name="TEMPERATURA_ENTRADA" expression="json-eval($.celsius)"/>
         </log>
         <payloadFactory media-type="xml">
            <format>
               <web:CelsiusToFahrenheit xmlns:web="http://www.w3schools.com/webservices/">
                  <web:Celsius>$1</web:Celsius>
               </web:CelsiusToFahrenheit>
            </format>
            <args>
               <arg evaluator="json" expression="$.celsius"/>
            </args>
         </payloadFactory>
         <header name="Action"
                 value="http://www.w3schools.com/webservices/CelsiusToFahrenheit"/>
      </inSequence>
      <outSequence>
         <log>
            <property xmlns:p="http://www.w3schools.com/webservices/"
                      name="TEMPERATURA_SALIDA"
                      expression="//p:CelsiusToFahrenheitResponse/p:CelsiusToFahrenheitResult"/>
         </log>
         <payloadFactory media-type="json">
            <format>
                            "Temperatura" : {
                                "EnFahrenheit" : $1
                            }
            </format>
            <args>
               <arg xmlns:p="http://www.w3schools.com/webservices/"
                    evaluator="xml"
                    expression="//p:CelsiusToFahrenheitResponse/p:CelsiusToFahrenheitResult"/>
            </args>
         </payloadFactory>
         <property name="messageType" value="application/json" scope="axis2"/>
         <send/>
      </outSequence>
   </target>
</proxy>

Para probar el servicio usamos el siguiente comando curl:

curl -X POST -H "Content-Type: application/json" -d "{ \"celsius\": \"20\" }" "http://localhost:8280/services/JsonToXMLProxy"

Y la respuesta es la siguiente:

"Temperatura" : {
                   "EnFahrenheit" : 68
                }


Al ESB llega un mensaje como el siguiente:

POST /services/JsonToXMLProxy HTTP/1.1
User-Agent: curl/7.25.0 (i386-pc-win32) libcurl/7.25.0 OpenSSL/0.9.8u zlib/1.2.6 libssh2/1.4.0
Host: 127.0.0.1:4444
Accept: */*
Content-Type: application/json
Content-Length: 19

{ "celsius": "20" }

Este mensaje json primero es capturado por un log que imprime en consola el valor de celsius usando la siguiente expresión: json-eval($.celsius)

2014-03-21 22:44:22,976] INFO - LogMediator To: /services/JsonToXMLProxy, MessageID: urn:uuid:60f0298e-f66d-4f9c-a2a9-5480ee2569a5, Direction: request, TEMPERATURA_ENTRADA = 20

Luego usando el mediador payloadfactory se crea un mensaje de acuerdo a la estructura que espera el servicio backend. Para saber su estructura siempre me apoyo en la herramienta SOAPUI, probando primero el servicio. El valor que se le pasa en el atributo se obtiene a partir de evaluar como json la expresión $.celsius.

Algo importante a tener en cuenta es especificar el Header, lo cual se hace usando el mediador header.
Y así de esta manera se termina la secuencia de entrada.

El mensaje creado es enviado al endpoint definido y su respuesta que viene en este formato, debe ser procesada:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:web="http://www.w3schools.com/webservices/">
   <soapenv:Body>
      <web:CelsiusToFahrenheitResponse>
         <!--Optional:-->
         <web:CelsiusToFahrenheitResult>68</web:CelsiusToFahrenheitResult>
      </web:CelsiusToFahrenheitResponse>
   </soapenv:Body>
</soapenv:Envelope>

Cuando este mensaje llega al ESB lo primero que hacemos es obtener en un log el valor de la temperatura, y se imprime como sigue en pantalla:

[2014-03-21 22:44:23,109] INFO - LogMediator To: http://www.w3.org/2005/08/addressing/anonymous, WSAction: , SOAPAction: , MessageID: urn:uuid:c76239bb-16cf-459d-99e5-5296d5aba938, Direction: response, TEMPERATURA_SALIDA = 68

Luego para crear la respuesta que enviaremos al cliente nos apoyamos en otro mediador payloadfactory en la secuencia de salida, definiendo que el tipo será json y especificando la estructura del mensaje de salida. El valor del argumento se obtiene a partir de conocer la estructura del mensaje SOAP que llega al ESB desde el servicio backend. Importante no dejar de incluir la propiedad de nombre messageType.

Y eso es todo, hay otras maneras de implementar este escenario bien sea usando el mediador script o el XSLT, pero en este caso me gusta más usar el payloadfactory.

Espero les sea de utilidad.

lunes, 17 de marzo de 2014

WSO2 API Manager + BAM. Monitorizando el consumo de las APIs

En la entrada anterior vimos como publicar como APIs en el WSO2 API Manager los servicios contenidos en una aplicación JAX-RS desplegada en el WSO2 Application Manager.

También vimos como desde una aplicación web en JAVA se accedían a dichas APIs y nos había quedado pendiente analizar la implementación a través del estudio del código fuente de ambas aplicaciones y el monitoreo de las APIs usando el WSO2 BAM.

En esta entrada veremos como monitorizar las APIs para lo cual haremos uso del siguiente enlace donde se explica paso a paso como configurar el BAM y el APIM.

El WSO2 API Manager a través de las funcionalidades que expone en la aplicación web API Manager nos permite ver los siguientes tipos de estadísticas:

  • Uso de todas las APIs y uso de todas las APIs por creador.
  • Promedio de los tiempos de respuesta.
  • Uso de un API por subscriptor.
  • Uso de un API por subscriptor y por versión.
  • Número de subscripciones por API.

De todas estas estadísticas la única disponible antes de configurar el BAM es el número de subscripciones por API. De ahí la importancia de incorporar el BAM a este escenario pues es la manera de saber cómo están siendo usadas y se están comportando nuestras APIs.

A partir de aquí asumimos que ambas herramientas han sido ya configuradas.
Si accedemos de nuevo a la aplicación web del API Publisher en https://localhost:9443/publisher y vamos a la opción “Suscripciones” debajo de “MIS APIS” veremos luego de varios consumos de la aplicación web en java como se rellenan las estadísticas. A manera de ejemplo les muestro las mías.



Podemos ver que cada una tiene una subscripción así que eso da un 33% a cada una.


Igual se muestra el número de llamadas por API.


Los tiempos de respuesta.


Últimos accesos.


Y el uso por usuario así como las invocaciones fallidas en caso de haberlas.


Así como pueden ver de una manera muy bien documentada por WSO2 podemos integrar el APIM con el BAM para lograr almacenar las estadísticas de uso de las APIs y llevar el control de su consumo.

A manera de señalamiento al parecer hay un problema con las fechas y hora en el APIM. Es algo que hay que revisar pero si se fijan en las estadísticas no están correctas las horas y las fechas que muestran.
Espero les sea de utilidad.

Hace poco más de un mes publiqué una entrada introductoria al WSO2 API Manager, APIM, que incluía un ejemplo como usar esta herramienta, el cual pueden consultar para entender lo que haré en esta entrada.
Básicamente estaremos viendo como exponer servicios usando el APIM y como consumirlos desde una aplicación web luego de autenticarnos.

La idea siempre parte de lo que se aprecia en la siguiente imagen y lo estaremos implementando paso a paso.



Paso 1: desplegar la aplicación que contiene los servicios en el WSO2 Application Server.
Nos descargamos el fuente desde esta ubicación  y en mi caso lo guardo en una carpeta que se llama “tutorial” y tendremos la siguiente estructura de carpetas.


En este paso trabajaremos con el “pizza-shack-api” así que entramos en el directorio hasta llegar a la  ruta [tutorial]\pizza-shack-api\src\main\java\com\pizzashack\ donde se puede observar los siguientes ficheros.



El único que he cambiado es el PizzaMenu.java debido a un problema con la localización, pues  da problema con el punto y la coma en el formato de los decimales.
Tan pronto lo descargan se ve así:
 
    private PizzaMenu() {
  Random rand = new Random();
  DecimalFormat format = new DecimalFormat("#.##");
  
  MenuItem item1 = new MenuItem();
  item1.setName("Chicken Parmesan");
  item1.setDescription("Grilled chicken, fresh tomatoes, feta and mozzarella cheese");
  item1.setPrice(format.format(rand.nextInt(20) + 10 - 0.01));
  item1.setIcon("/images/1.png");
  items.add(item1);


Luego de realizar el ajuste queda así:
 
 private PizzaMenu() {
  Random rand = new Random();
        DecimalFormat format = new DecimalFormat("#.##", new DecimalFormatSymbols(Locale.ENGLISH));

  MenuItem item1 = new MenuItem();
  item1.setName("Chicken Parmesan");
  item1.setDescription("Grilled chicken, fresh tomatoes, feta and mozzarella cheese");
  item1.setPrice(format.format(rand.nextInt(20) + 10 - 0.01));
  item1.setIcon("/images/1.png");
  items.add(item1);  
  


Una vez hecho este cambio vamos a la raíz del proyecto y ejecutamos el comando Maven mvn clean package para que nos cree en la carpeta target el fichero pizzashack-api-1.0.0.war que será la aplicación web que debemos  desplegar en el WSO2 Application Server.
Iniciamos el servidor AS y bien podemos copiar  el fichero en la ubicación [AS]\repository\deployment\server\webapps\ y entrar a través de la interfaz gráfica de la herramienta e ir hasta Home> Manage > Applications > Add> JAX-WS/JAX-RS, Y agregarla por esa vía.



En ambos casos ya tendrán desplegada la aplicación que contiene el servicio, tal y como pueden ver a continuación.


Al dar clic en el contexto vemos el dashboard de la aplicación.



Aquí pueden ver las opciones para generar un cliente, copiar su endpoint o ver el diseño del mismo dando clic en WADL.
Volviendo atrás, si dan clic en “Find Services”  podrán ver el servicio que se implementa.



Y al dar clic en el enlace tenemos acceso a la descripción del servicio.
Si se fijan verán un servicio para obtener el menú a través de esta URL: http://127.0.0.1:9765/pizzashack-api-1.0.0/api/menu/  si lo probamos con un cliente veremos lo siguiente:



Nos devuelve el nombre del plato, una descripción, un icono para que sea puesto en la aplicación web que  consuma de este servicio y el precio.
Los invito llegado este punto que prueben el resto de los servicios para que vean cómo funcionan, así como revisar el fuente de cada servicio para que dominen las operaciones implementadas.

Paso 2: publicar estos servicios como APIs en el WSO2 API Manager.
El objetivo de este paso es publicar los servicios como APIs, y para ello debemos acceder al WSO2 API-M Publisher a través de esta URL: https://localhost:9443/publisher
 



Una vez autenticados debemos ir hasta   APIs / Add New API tal y como muestra la siguiente imagen.



Ahora creams las 3 APIs una por cada servicio usando la siguiente información:

Delivery API

                API Name= pizzaShack
                Context = /pizzashack/delivery
                Version = 1.0.0
                Production Endpoint URL=http://localhost:9765/pizzashack-api-1.0.0/api/delivery
                API Resources = Dejar los valores por defecto

Order API
               
                API Name= pizzashack-order
                Context = /pizzashack/order
                Version = 1.0.0
                Production Endpoint URL=http://localhost:9765/pizzashack-api-1.0.0/api/order
                API Resources = Dejar los valores por defecto

Menu API

                API Name= pizzashack-menu
                Context = /pizzashack/menu
                Version = 1.0.0
                Production Endpoint URL=http://localhost:9765/pizzashack-api-1.0.0/api/menu
                API Resources = Dejar los valores por defecto

Una vez terminada esta acción tendremos todas las APIs ya creadas y visibles a través del Browser.



CREATED significa que están en el estado de creadas. Ahora debemos entrar a cada una y modificar el estado en la opción del ciclo de vida para ponerlas como publicadas.




Si juegan un poco con el ciclo de vida verán que se lleva un control de las acciones realizadas. Algo muy útil para saber que ha pasado con el API en todo momento.
Luego de cambiar el estado de las 3 APIs se visualizan de la siguiente manera.



Pueden observar cómo ha cambiado su estado.
Así de esta manera hemos creado las 3 APIs y las hemos publicado para que estén visibles en el próximo paso.


Paso 3: Crear una aplicación y subscribir las APIs.

Lo primero es ir al Store del WSO2 API Manager, para poder configurar adecuadamente el consumo de las APIs. Deben acceder a la siguiente URL: https://localhost:9443/store
Tan pronto nos autenticamos en el Store podemos ver las 3 APIs creadas en el paso anterior.




Ahora debemos dar clic en una de las APIs, y en el combo de las aplicaciones seleccionar la aplicación por defecto o una que ustedes creen. En este ámbito se entiende por aplicación a la agrupación lógica de un conjunto de APIs sobre la cual realizaremos determinadas acciones. En mi caso creo una nueva aplicación con el nombre de “Pizza” y accediendo a cada API selecciono en el combobox de las aplicaciones la que he creado.
Al finalizar esta acción las 3 APIs están vinculadas con mi aplicación “Pizza”. La forma de ver esto es ir a la opción “My Subscriptions” en el menú superior y verán seleccionada en un combobox la aplicación y se van al final de la página verán lo siguiente.


Al inicio de la página tenemos esta configuración:



La cual es muy importante pues nos generará las claves para consumir las APIs, así que le damos al botón “Generate” y obtenemos lo siguiente:



Este token que hemos generado es del tipo “Application Access Tokens”. La herramienta WSO2 API Manager permite generar además de este tipo de token un “User Access Tokens”.
La diferencia es que el primero nos permite identificar y autenticar una aplicación completa, mientras que el segundo es para identificar y autenticar al usuario final de una aplicación. Usamos el primer token porque nos permite con un solo token acceder a todas las APIs de la aplicación “Pizza”.
Ahora podemos probar el consumo de una API usando curl con el siguiente comando:
curl -k -H "Authorization: Bearer c4H4fMWbPGw8MNY5gVM9s_DAgXoa" https://localhost:8245/pizzashack/menu/1.0.0   resalto en negrita y subrayado el token que debe corresponderse con el valor del “Access Token” que pueden observar en la imagen arriba.
Esto  nos devuelve lo siguiente.



Con lo cual ya sabemos que nuestras APIs están online.

Paso 4: configurar y desplegar una aplicación web en java que consuma las APIs usando las claves generadas.

Llegado este momento debemos ir al código de la aplicación web que se encuentra en el directorio “pizza-shack-web” y ajustar los parámetros necesarios en el fichero web.xml que se encuentra en [pizza-shack-web]\src\main\webapp\WEB-INF\web.xml

<web-app xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
 version="2.4">
 <display-name>Pizza Shack Client App</display-name>

 <listener>
  <listener-class>com.pizzashack.client.web.PizzaShackContextListener</listener-class>
 </listener>
 
 <context-param>
        <param-name>serverURL</param-name>
        <param-value>http://localhost:8280/pizzashack</param-value>
    </context-param>
    <context-param>
        <param-name>loginURL</param-name>
        <param-value>http://localhost:8280/token</param-value>
    </context-param>
    <context-param>
        <param-name>consumerKey</param-name>
        <param-value>yOJ2gUwZctQ44rZYnb2BM88bUG0a</param-value>
    </context-param>
    <context-param>
        <param-name>consumerSecret</param-name>
        <param-value>4uUpUtHc7rvNknG1QqPVHLdCneIa</param-value>
    </context-param>
 
</web-app>


Luego vamos a la raíz del directorio y ejecutamos el comando Maven mvn clean package.
Esta acción nos genera la aplicación pizzashack.war  que procedemos a desplegar también en el WSO2 Application Server.
Accedemos al WSO2 AS y vamos hasta: Home> Manage > Applications> Add> Web Applications buscamos el .war recién creado y le damos al botón “Upload”.
Como la aplicación web recientemente subida al AS tiene entre sus parámetros el consumerKey y el consumerSecret la aplicación puede generar token específicos para los usuarios y así de esta manera se restringe el acceso a las APIs.


Paso 5: acceder a la aplicación y consumir las APIs.
Ahora procedemos al siguiente enlace: http://localhost:9765/pizzashack/login.jsp y ponemos las credenciales admin/admin
El sistema nos deja entrar y podemos ver la siguiente pantalla, realmente deliciosa    :-D



Ordenamos una pizza.



Y se nos genera un número de identificación.



Con este número vamos al menú superior y seleccionamos  “My Orders”.



Introducimos el número y le damos “Find order”.




Como tenemos acceso al código fuente pues podemos ver la implementación tanto de la aplicación JAX-RS como de la aplicación web en JAVA y el consumo de las APIs, esto lo estaremos analizando en entradas siguientes. Pero igual nos sigue quedando pendiente el tema del monitoreo del consumo de las APIs lo cual realizaremos en la siguiente entrada  conectando a esta solución el WSO2 BAM.

Espero les sea de utilidad.

Algunos enlaces que le pueden servir.

viernes, 14 de marzo de 2014

El ESB de WSO2 se considera el ESB más rápido en su categoría

Aunque la noticia es de mediados del mes pasado creo que es válido seguirla reproduciendo por su importancia.

Luego de realizada un ronda de pruebas entre varios ESB la siguiente gráfica refleja el comportamiento de cada cual en diferentes escenarios probados. De ser válidos los resultados no cabe duda de la afirmación que da título a esta entrada, y nos demuestra a los seguidores de la suite de WSO2 que no estamos equivocados en nuestra elección.

Solo en las pruebas de seguridad no fue  el más rápido de todos, aunque se puede apreciar que la differencia fue relativamente pequeña.

lunes, 10 de marzo de 2014

Interoperabilidad a través de un Modelo de Datos Canónicos


En temas de interoperabilidad técnica, relacionada con SOA, el patrón Modelo de Datos Canónicos reviste un especial interés, ya que cuando fue elaborado se pensó como una "bala de plata" para los temas de integración e interoperabilidad, pero hasta el día de  hoy tiene sus pro y sus contras, como todo.

Según la wikipedia este patrón permite la comunicación entre sistemas que tienen diferentes modelos de datos, reduciendo costos y estandarizando la forma de comunicarse. Su adopción pasa por el uso de algún middleware, ESB, que permita implementar el patrón con lo cual las aplicaciones se desentienden de como transformar sus datos a las estructuras de datos de otras aplicaciones.

Veamos con un ejemplo de la vida real como funciona.

Digamos que tenemos una persona X que habla español y una persona Y que habla chino.
La unica forma que tienen estas dos personas de entenderse entre si es que la información intercambiada sea transformada de un idioma a otro, bien sea del español al chino o de chino al español.

Si agregamos 20 personas más, cada una con su idioma propio y sin repetir, pues cada persona tendrá que saber como transformar la información que envía al idioma de la persona que debe recibirlo, para que se puedan comunicar.

Algo similar es lo que se presenta cuando múltiples aplicaciones tienen que intercambiar información entre si,  ya que deben implementar transformaciones para los mensajes que se mandan o se reciben.
En nuestro ejemplo de las personas la solución es usar un idioma común para todas, algo como el Esperanto. De esta manera cada persona solo tiene que saber su idioma y el Esperanto, y eso es todo. Si tiene que hablar con otra persona que no domine su idioma usa el Esperanto y así se podrán entender.

Llevando esto al ámbito técnico la solución es usar el patrón Modelo de Datos Canónicos (MDC), el cual nos indica que debemos crear un modelo que sea común a todos los sistemas, de forma tal que se mapeen en él las principales entidades y sus atributos. De esta manera si  una aplicación X quiere enviar un objeto Persona a una aplicación Y, debe convertir su objeto Persona al objeto Persona del MDC para poder enviarlo a la aplicación destino. La aplicación destino entonces convertirá el objeto persona del MDC a su modelo propio y lo podrá usar. Algunas ideas al respecto las podrán encontrar en este enlace y en este otro.

 Por lo general este patrón es bueno usarlo en organizaciones que poseen múltiples sistemas legados que además tienen diferentes BD con diferentes esquemas y modelado de las entidades de negocio, pues es el escenario ideal para su implementación cuando se requiera alguna solución de integración. Este patrón y su implementación vienen también con formas negativas de implementarlo, por lo que es importante primero entenderlo adecuadamente, ver las buenas prácticas a seguir y contar con el apoyo de la organización para poder ejecutarlo, de lo contrario puede ser todo lo contrario de lo propuesto  inicialmente. Información al respecto la pueden encontrar en este enlace.

Por último mi recomendación es que se estudien el libro "Enterprise Integration Patterns: Designing, Building, and Deploying Messaging Solutions" de Gregor Hohpe y Bobby Woolf donde se explica correctamente este patrón.

En la siguiente entrada sobre este tema estaremos viendo como WSO2 plantea el tema del MDC usando su ESB

viernes, 7 de marzo de 2014

Como ya habíamos visto en la entrada anterior, el WSO2 Identity Server tiene entre sus múltiples funcionalidades relacionadas con la seguridad la de actuar como un Identity Provider o IDP.
Se había dejado como pendiente la captura de los atributos contenidos en el almacén de usuarios que estuviera usando el  WSO2 Identity Server y eso será lo que haremos en esta entrada.

El escenario:
  • Digamos que tenemos inicialmente el WSO2 configurado para que use su BD interna de los usuarios, pero queremos que también use un LDAP como almacén de usuarios, pueden usar este enlace para configurar su LDAP y de esta manera se podrán autenticar en las aplicaciones usando los usuarios del LDAP.
  • El Identity server tiene un conjunto de usuarios, los cuales tienen atributos definidos a través de la funcionalidad “My Profile” y el LDAP contiene información de los usuarios la cual depende de la implementación de cada organización y la información que se desee guardar en el LDAP.
  • Se desea que las aplicaciones que participen en el escenario de Single Sign On usando el WSO2 Identity Server puedan obtener los atributos requeridos del usuario autenticado en ellas, bien sea desde el almacén por defecto de la herramienta o bien desde el LDAP.

Veamos cómo se hace:

Lo primero es ir al WSO2 Identity Server y editar la configuración del SSO para la aplicación que requiere dichos atributos. Noten el valor del “Consumer Index” pues luego hará falta.


Cuando entramos en  su configuración debemos marcar la opción que dice “Enable Attribute Profile”:






Y en la parte de Claim seleccionamos aquellas que se correspondan con los atributos que sabemos están en el LDAP y le damos al botón “Add Claim” y así veremos cómo se van agregando los atributos.

NOTA: El WSO2 Identity Server también tiene la posibilidad de gestionar estos atributos, lo cual es muy útil para saber que Claims se corresponden con qué atributos en el LDAP o para agregar nuevas en caso de ser requerido. En el caso del rol y de la URL tuve que realizar ajustes pues no se correspondían con el esquema de atributos soportados por el LDAP.

Cuando nos volvamos a autenticar en la aplicación que usamos de ejemplo veremos entonces los valores de  los atributos.

Mi primera prueba fue con un usuario interno, en este caso el admin de la herramienta y tuve que llenar en “My Profile” los datos que se querían mostrar.



Luego al volver a autenticarme con las credenciales del LDAP se cargaron mis atributos sin problema desde el LDAP. Aquí aconsejo revisar el LDAP de cada cual y ver los atributos que tienen sus usuarios, luego revisar los claims que tengan y ver si tienen para obtener esos atributos. En caso de que no los tengan bien pueden modificar los existentes o agregar nuevos.

Para capturar estos datos se usó la siguiente página JSP:


En el caso de la aplicación PHP el escenario es bastante similar. Hay que editar la configuración del SSO para dicha aplicación y seleccionar los atributos que queremos obtener.

Al revisar el código de la aplicación demo vemos que también tiene implementada la captura de los atributos, como se puede ver en la siguiente imagen:




Pero al probarla no se mostraba nada. Así que volví a repetir el mismo procedimiento de la entrada anterior y comparar los mensajes SAML intercambiados, y resulta que mientras la aplicación JAVA está enviando un atributo AttributeConsumingServiceIndex con el identificador que se muestra en su configuración de SSO en el WSO2 Identity Server, la aplicación web en PHP no lo hacía, por lo que tuve que modificar el código.

En el fichero Settings.php agregué un nuevo atributo AttributeConsumingServiceIndex a la clase OneLogin_Saml_Settings tal y como se muestra en esta imagen:




Luego  en el fichero settings.php le seteo el valor correspondiente:


Y por último en el fichero AuthRequest.php modifico la estructura del mensaje SAML a enviar para que incluya este atributo:



        $request = <<<AUTHNREQUEST
<samlp:AuthnRequest
    xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
    xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
    ID="$id"
    Version="2.0"
    IssueInstant="$issueInstant"
 Destination="{$this->_settings->idpSingleSignOnUrl}"
    ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
    AssertionConsumerServiceURL="{$this->_settings->spReturnUrl}"
 AttributeConsumingServiceIndex="{$this->_settings->AttributeConsumingServiceIndex}">
    <saml:Issuer>{$this->_settings->spIssuer}</saml:Issuer>
    <samlp:NameIDPolicy
        Format="{$this->_settings->requestedNameIdFormat}"
        AllowCreate="true"></samlp:NameIDPolicy>
    <samlp:RequestedAuthnContext Comparison="exact">
        <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef>
    </samlp:RequestedAuthnContext>
</samlp:AuthnRequest>
AUTHNREQUEST;


De esta manera al tratar de acceder a la aplicación php vemos como el mensaje llega al WSO2 Identity Server con el atributo requerido y también podemos ver como el mensaje SAML de respuesta contiene los valores de los atributos solicitados, pero llegado este punto tampoco se muestran en la página web. De nuevo a revisar código PHP  :-(


Aquí les dejo un fragmento del mensaje obtenido desde el WSO2 Identity Server, es solo un fragmento pues el mensaje es bastante grande:
<saml2:AttributeStatement>
<saml2:Attribute Name="http://wso2.org/claims/country"><saml2:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">Cuba</saml2:AttributeValue></saml2:Attribute>
<saml2:Attribute Name="http://wso2.org/claims/mobile"><saml2:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">+5353722213</saml2:AttributeValue></saml2:Attribute>
<saml2:Attribute Name="http://wso2.org/claims/role"><saml2:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">admin,Internal/everyone</saml2:AttributeValue></saml2:Attribute>
<saml2:Attribute Name="http://wso2.org/claims/url"><saml2:AttributeValue xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">http://desarrollosoa.blogspot.com/</saml2:AttributeValue></saml2:Attribute>
</saml2:AttributeStatement>



Luego de revisar el código php noté que había un problema con el fichero Response.php que contiene la clase OneLogin_Saml_Response y es que esta clase no capturaba los datos de los mensajes SAML2, solo los SAML, así que la modifiqué de esta manera.

<?php

/**
 * Parse the SAML response and maintain the XML for it.
 */
class OneLogin_Saml_Response
{
    /**
     * @var OneLogin_Saml_Settings
     */
    protected $_settings;

    /**
     * The decoded, unprocessed XML assertion provided to the constructor.
     * @var string
     */
    public $assertion;

    /**
     * A DOMDocument class loaded from the $assertion.
     * @var DomDocument
     */
    public $document;

    /**
     * Construct the response object.
     *
     * @param OneLogin_Saml_Settings $settings Settings containing the necessary X.509 certificate to decode the XML.
     * @param string $assertion A UUEncoded SAML assertion from the IdP.
     */
    public function __construct(OneLogin_Saml_Settings $settings, $assertion)
    {
        $this->_settings = $settings;
        $this->assertion = base64_decode($assertion);
        $this->document = new DOMDocument();
        $this->document->loadXML($this->assertion);
    }

    /**
     * Determine if the SAML Response is valid using the certificate.
     *
     * @throws Exception
     * @return bool Validate the document
     */
    public function isValid()
    {
        $xmlSec = new OneLogin_Saml_XmlSec($this->_settings, $this);
        return $xmlSec->isValid();
    }

    /**
     * Get the NameID provided by the SAML response from the IdP.
     */
    public function getNameId()
    {
        $entries = $this->_queryAssertion('/saml2:Subject/saml2:NameID');
        return $entries->item(0)->nodeValue;
    }

    /**
     * Get the SessionNotOnOrAfter attribute, as Unix Epoc, from the
     * AuthnStatement element.
     * Using this attribute, the IdP suggests the local session expiration
     * time.
     * 
     * @return The SessionNotOnOrAfter as unix epoc or NULL if not present
     */
    public function getSessionNotOnOrAfter()
    {
        $entries = $this->_queryAssertion('/saml2:AuthnStatement[@SessionNotOnOrAfter]');
        if ($entries->length == 0) {
            return NULL;
        }
        $notOnOrAfter = $entries->item(0)->getAttribute('SessionNotOnOrAfter');
        return strtotime($notOnOrAfter);
    }

    public function getAttributes()
    {
        $entries = $this->_queryAssertion('/saml2:AttributeStatement/saml2:Attribute');

        $attributes = array();
        /** @var $entry DOMNode */
        foreach ($entries as $entry) {
            $attributeName = $entry->attributes->getNamedItem('Name')->nodeValue;

            $attributeValues = array();
            foreach ($entry->childNodes as $childNode) {
                if ($childNode->nodeType == XML_ELEMENT_NODE && $childNode->tagName === 'saml2:AttributeValue'){
                    $attributeValues[] = $childNode->nodeValue;
                }
            }

            $attributes[$attributeName] = $attributeValues;
        }
        return $attributes;
    }

    /**
     * @param string $assertionXpath
     * @return DOMNodeList
     */
    protected function _queryAssertion($assertionXpath)
    {
        $xpath = new DOMXPath($this->document);
        $xpath->registerNamespace('samlp'   , 'urn:oasis:names:tc:SAML:2.0:protocol');
        $xpath->registerNamespace('saml'    , 'urn:oasis:names:tc:SAML:2.0:assertion');
        $xpath->registerNamespace('ds'      , 'http://www.w3.org/2000/09/xmldsig#');
  $xpath->registerNamespace('saml2'   , 'urn:oasis:names:tc:SAML:2.0:assertion');

        $signatureQuery = '/samlp:Response/saml:Assertion/ds:Signature/ds:SignedInfo/ds:Reference';
        $assertionReferenceNode = $xpath->query($signatureQuery)->item(0);
        if (!$assertionReferenceNode) {
            throw new Exception('Unable to query assertion, no Signature Reference found?');
        }
        $id = substr($assertionReferenceNode->attributes->getNamedItem('URI')->nodeValue, 1);

        $nameQuery = "/samlp:Response/saml:Assertion[@ID='$id']" . $assertionXpath;
        return $xpath->query($nameQuery);
    }
}


Así fue entonces como desde la aplicación web en PHP pude mostrar los valores de los atributos.



Es válido aclarar que este problema no es generado por el WSO2 Identity Server, sino por el código PHP empleado en este ejemplo.

Espero les sea de utilidad.

miércoles, 5 de marzo de 2014

WSO2 como proveedor de SSO en la práctica usando el Identity Server


Todos aquellos que han podido experimentar con la herramienta WSO2 Identity Server saben que esta puede actuar como un servidor de Single Sign On bien usando OpenID o SAML.

Para resumir, el SSO nos permite acceder usando una sola cuenta a diferentes sistemas autenticándonos solo en uno de ellos, el resto se comunica con un servidor de SSO que les provee las credenciales que hayamos introducido anteriormente o les transfiere un token mediante el cual es confirma que nos hemos autenticado previamente.

De esta forma se evita  que los usuarios tengan una cuenta por sistema y que se tengan que autenticar en cada sistema al que deseen entrar.

Claro que previamente todos estos sistemas deben haberse configurado para lograr este escenario y es lo que veremos en esta entrada.

En lo personal había probado el Identity Server de WSO2 para establecer SSO entre los mismos servidores de la suite y en una PoC para aplicaciones web en JAVA, pero no había probado hasta el momento con aplicaciones web en PHP. Hasta hace poco usaba el CAS para montar escenarios de SSO.

El escenario es el siguiente:
  • Se tiene una aplicación web en JAVA desplegada en un servidor tomcat y se tiene una aplicación web en PHP desplegada en un servidor Apache.
  • Se desea proporcionar un ambiente de SSO para que los usuarios una vez autenticados en cualquiera de las 2 aplicaciones tengan acceso a la otra sin tener que volverse a autenticar.

Para aplicaciones web en JAVA solo basta con seguir los pasos de este ejemplo
Lo principal a tener en cuenta es:

Tener en el pom de nuestra webapp la siguiente dependencia:

    <dependencies>
        <dependency>
            <groupId>org.wso2.carbon</groupId>
            <artifactId>org.wso2.carbon.identity.sso.agent</artifactId>
            <version>1.0.0</version>
        </dependency>
    </dependencies>

Y en el web.xml el siguiente filtro:
    <filter>
        <filter-name>SSOFilter</filter-name>
        <filter-class>org.wso2.carbon.identity.sso.agent.SSOAgentFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>SSOFilter</filter-name>
        <url-pattern>*.jsp</url-pattern>
        <url-pattern>/samlsso</url-pattern>
        <url-pattern>/openid</url-pattern>
        <url-pattern>/logout</url-pattern>
    </filter-mapping>

Por último deben configurar bien el fichero de propiedades para que la aplicación pueda redireccionar hacia donde tengan ubicado el WSO2 IS y se pueda establecer la configuración.

Realmente los pasos son muy sencillos y no dan motivo de pérdida.

Les dejo una imagen de la configuración hecha en el WSO2 IS para esta aplicación:


Luego de tener la webapp en JAVA funcionando me decidí a implementar lo mismo para PHP.

Estuve revisando algunas implementaciones para SAML y finalmente me opté por  ONELOGIN http://support.onelogin.com/entries/268420-saml-toolkit-for-php
Debo reconocer que de PHP no veía nada desde hace ya 9 años y que la elección fue sin muchos criterios a tener en cuenta, pero el uso de la aplicación demo fue bastante sencillo.

Descargue de github la herramienta https://codeload.github.com/onelogin/php-saml/zip/master que dentro tiene un demo.

Instalé WAMP, para así tener mi servidor APACHE con PHP y copié para la carpeta www la aplicación de ONELOGIN luego de descompactarlo.

Los cambios que hice fueron inicialmente en el fichero settings.php que queda como sigue para mi escenario:
<?php
/**
 * SAMPLE Code to demonstrate how provide SAML settings.
 *
 * The settings are contained within a OneLogin_Saml_Settings object. You need to
 * provide, at a minimum, the following things:
 *
 *  - idpSingleSignOnUrl
 *    This is the URL to forward to for auth requests.
 *    It will be provided by your IdP.
 *
 *  - idpPublicCertificate
 *    This is a certificate required to authenticate your request.
 *    This certificate should be provided by your IdP.
 * 
 *  - spReturnUrl
 *    The URL that the IdP should redirect to once the authorization is complete.
 *    You must provide this, and it should point to the consume.php script or its equivalent.
 */

define('XMLSECLIBS_DIR', './../ext/xmlseclibs/');
require_once XMLSECLIBS_DIR . 'xmlseclibs.php';

define('ONELOGIN_SAML_DIR', './../src/OneLogin/Saml/');
require_once ONELOGIN_SAML_DIR . 'AuthRequest.php';
require_once ONELOGIN_SAML_DIR . 'Response.php';
require_once ONELOGIN_SAML_DIR . 'Settings.php';
require_once ONELOGIN_SAML_DIR . 'XmlSec.php';

$settings = new OneLogin_Saml_Settings();

// When using Service Provider Initiated SSO (starting at index.php), this URL asks the IdP to authenticate the user.
//$settings->idpSingleSignOnUrl = 'https://app.onelogin.com/saml/signon/6171';
$settings->idpSingleSignOnUrl = 'https://localhost:9443/samlsso';

// The certificate for the users account in the IdP
$settings->idpPublicCertificate = <<<CERTIFICATE
-----BEGIN CERTIFICATE-----
MIICNTCCAZ6gAwIBAgIES343gjANBgkqhkiG9w0BAQUFADBVMQswCQYDVQQGEwJVUzELMAkGA1UE
CAwCQ0ExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxDTALBgNVBAoMBFdTTzIxEjAQBgNVBAMMCWxv
Y2FsaG9zdDAeFw0xMDAyMTkwNzAyMjZaFw0zNTAyMTMwNzAyMjZaMFUxCzAJBgNVBAYTAlVTMQsw
CQYDVQQIDAJDQTEWMBQGA1UEBwwNTW91bnRhaW4gVmlldzENMAsGA1UECgwEV1NPMjESMBAGA1UE
AwwJbG9jYWxob3N0MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCUp/oV1vWc8/TkQSiAvTou
sMzOM4asB2iltr2QKozni5aVFu818MpOLZIr8LMnTzWllJvvaA5RAAdpbECb+48FjbBe0hseUdN5
HpwvnH/DW8ZccGvk53I6Orq7hLCv1ZHtuOCokghz/ATrhyPq+QktMfXnRS4HrKGJTzxaCcU7OQID
AQABoxIwEDAOBgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADgYEAW5wPR7cr1LAdq+IrR44i
QlRG5ITCZXY9hI0PygLP2rHANh+PYfTmxbuOnykNGyhM6FjFLbW2uZHQTY1jMrPprjOrmyK5sjJR
O4d1DeGHT/YnIjs9JogRKv4XHECwLtIVdAbIdWHEtVZJyMSktcyysFcvuhPQK8Qc/E/Wq8uHSCo=
-----END CERTIFICATE-----
CERTIFICATE;

// The URL where to the SAML Response/SAML Assertion will be posted
$settings->spReturnUrl = 'http://localhost/php-saml/demo/consume.php';

// Name of this application
$settings->spIssuer = 'php-saml';

// Mio
$settings->destination = 'https://localhost:9443/samlsso';

// Tells the IdP to return the email address of the current user
$settings->requestedNameIdFormat = OneLogin_Saml_Settings::NAMEID_EMAIL_ADDRESS;

return $settings;

Lo siguiente que hice fue ir al WSO2 IS y configurar esta aplicación web de la misma forma en que ya había configurado la aplicación para JAVA, cambiando claro los nombres de algunos campos.

Al intentar autenticarme en la aplicación PHP la redirección funcionó sin problemas, el usuario se autenticó en el IS pero me dio un error en la misma página de autenticación del WSO2 IS.
El error era que me faltaba un atributo en el SAML2 request que estaba enviando la aplicación web al WSO2 IS. En StackOverflow pueden ver mi pregunta y los avances que hice hasta llegar a la solución http://stackoverflow.com/questions/22182354/sso-for-php-webapp-with-wso2-identity-server-authentication-request-failed/

Finalmente cuando pude pasar este error me dio otro relacionado con la validación de la firma y que pueden ver al final del enlace anterior. Para evitarlo tuve que desmarcar la opción de validación y por eso mi configuración para la webapp en PHP dentro del WSO2 IS queda de la siguiente manera:


Para finalizar probé autenticarme en una aplicación y luego acceder a la otra, pude comprobar que no me pedía autenticación. Que era lo que estaba buscando.

Como elementos pendientes quedan:
  • Obtener atributos de los usuarios desde el IS para propagarlos hacia las aplicaciones web.
  • Configurar el single sing out en la aplicación web en PHP y probar este mecanismo. La idea es que si me des autentico de una aplicación lo mismo debe pasar en el resto.
En otras entradas estaremos viendo ambos elementos.
Espero les sea de utilidad.

lunes, 3 de marzo de 2014

WSO2 y el filtrado de las acciones en los servicios.

Hace poco en un curso que estaba dando sobre el ESB de WSO2 le comentaba a los estudiantes que se presentan ocasiones en que el servicio backend que queremos exponer a través de un servicio proxy tiene más operaciones que las que deseamos sean expuestas, y que hay varias formas de implementar para evitar que las mismas sean accedidas desde el servicio proxy.

Una variante sencilla es no exponer estas operaciones vía el WSDL del servicio proxy, de esta manera quienes estén creando un cliente del servicio proxy no verán las operaciones.

Otra variante puede ser una autorización a nivel de operaciones, aunque en este caso me queda la duda de si se puede lograr fácilmente pues las políticas de seguridad en el ESB se adjuntan al binding y no a las operaciones.

Otra variante es implementar un mecanismo de autorización de grano fino, donde por intermedio del mediador Entitlement podamos consultar políticas de seguridad definidas en el Identity Server de WSO2 y de esta manera determinar qué usuarios y/o roles y bajo cuales circunstancias tienen acceso a determinadas operaciones. Creo que es la solución más fuerte.

Otra variante si no quieren complicarse mucho es ir al dashboard del servicio y en la sección de “Quality of Service Configuration” pinchar donde dice “Operations


Seleccionar la operación a la cual desean denegar el acceso:

Y verán un dashboard solo para esta operación.


Ahí deben pinchar donde dice “Access Throttling” y llegaran a una pantalla como esta.

Si lo ponen tal y como se les muestra será imposible consumir esta operación. En la consola de la herramienta saldrá algo como lo siguiente:


Si tratan de consumir la operación desde un cliente, en mi caso uso el SOAPUI, verán lo siguiente:




Evidentemente estamos prohibiendo el acceso desde cualquier IP a esta operación. Si prueban las otras funcionarán sin problemas.

La mejor variante será la que mejor se adapte a sus requerimientos e igual se puede dar el caso en que haya que combinar 2 o más variantes.

Lo que no me ha gustado de esta configuración de filtrado, es que si el filtrado propuesto lo hacemos en un servicio backend  y luego creamos un proxy de este servicio, cuando invocamos a la operación no obtenemos nada como respuesta, o sea el mensaje de fallo no se propaga como corresponde. En mi caso estoy usando un ESB con las features del AS embebidas, pero no creo que ese sea el problema. He movido este bloqueo para el servicio proxy y con los mismos resultados. Solo en pocas ocasiones me ha devuelto un mensaje de fallo, pero nunca ha pasado por la secuencia de fallo que le definí.

Ilustrando este problema:
Imagen de cuando si retorna un mensaje fault el servicio proxy:


En la siguiente llamada falla y es un comportamiento aleatorio: