miércoles, 26 de agosto de 2009

Autenticación y autorización de webservices con certificados en GlassFish

Copyright © 2009 Juan Andrés Ghigliazza.
Se concede permiso para copiar, distribuir y/o modificar este documento bajo los términos de la Licencia de Documentación Libre de
GNU, Versión 1.3 o cualquier otra versión posterior publicada por la Free Software Foundation; sin Secciones Invariantes ni Textos de Cubierta Delantera ni Textos de Cubierta Trasera.
Una copia de la licencia se puede ver aquí.

1 Introducción

En este documento, se explica por medio de un ejemplo, como configurar el acceso seguro mediante certificados a un webservice de GlassFish. El ejemplo no solo utiliza certificados X.509 para autenticación bi-direccional (tanto del servidor como del cliente), sino que también usa la información del certificado del cliente para autorizar el uso de determinados recursos.

Algunos datos del software usado:

IDE

NetBeans IDE 6.5

Servidor de aplicaciones

GlassFish (Sun Java System Application Server 9.1_02)

OS

Ubuntu 8.04.3

Java

1.6.0_14 (Sun)

2 Caso de ejemplo

El ejemplo usado es un webservice muy simple, el cual recibe como parámetro un string, y devuelve otro string concatenado con el que recibió. En esta sección veremos como implementar tanto el webservice, como un cliente.

2.1 Construcción del webservice

  • Crear un nuevo proyecto en NetBeans, del tipo "Jave EE" - "EJB Module"1. Llamarlo WebServiceSimple.

  • Crear un nuevo webservice haciendo click derecho en el proyecto, "New" - "Web Service". Llamarlo ServicioSimple y al paquete prueba2.

  • Dentro del nuevo webservice, crear el método metodoSimple. El código completo del webservice quedaría de la siguiente forma:

package prueba; 

import javax.jws.WebService; 
import javax.ejb.Stateless; 

@WebService() 
@Stateless() 
public class ServicioSimple { 

    public String metodoSimple(String mensaje) { 
        return "Mensaje recibido por el webservice: '" + mensaje + "'."; 
    } 
    
} 

2.2 Creación de un cliente

Creación de una aplicación JSE cliente del webservice:

  • Crear un nuevo proyecto "Java" - "Java Application". Llamarlo WebServiceSimpleCliente.

  • Agregar un cliente del webservice en el proyecto. Para ello:

    • Hacer click derecho sobre el proyecto, y "New" - "Web Service Client".

    • Elegir "WSDL URL" e ingresar la dirección del WSDL antes vista.

  • Luego, en el programa principal (clase generada por NetBeans, que es webservicesimplecliente.Main), agregar la llamada al webservice, desplegando sus resultados. Para ello:

    • En el código, dentro del método main hacer click derecho, "Web Service Client Resources" - "Call Web Service Operation".

    • Elegir el método del webservice: "WebServiceSimpleCliente" - "ServicioSimple" - "ServicioSimpleService" - "ServicioSimplePort" – "metodoSimple".

  • Luego modificar un poco la llamada generada para mandar un texto arbitrario, e imprimir el resultado. La clase quedaría más o menos de la siguiente forma:

package webservicesimplecliente;

public class Main {

    public static void main(String[] args) throws Exception {
        prueba.ServicioSimpleService service = new prueba.ServicioSimpleService();
        prueba.ServicioSimple port = service.getServicioSimplePort();
        String result = port.metodoSimple("Envío texto al webservice.");
        System.out.println("Texto devuelto por el webservice = " + result);
    }
}
  • Luego se puede correr el cliente; la salida debería ser algo como:

Texto devuelto por el webservice = Mensaje recibido por el webservice: 'Envío texto al webservice.'.

En este momento ya tenemos implementados tanto el webservice de prueba, como el cliente, y funcionando. De aquí en más veremos como configurar los certificados.

3 Certificados

En esta sección veremos como configurar los certificados tanto en el servidor como en el cliente. Los certificados usados serán para el servidor, el que genera automáticamente GlassFish al momento de su instalación, y para el cliente crearemos uno autofirmado; cada uno de ellos se copiará a los almacenes de claves (keystore) de confianza del otro. Está fuera del alcance de este artículo el uso de una PKI (Public Key Infrastructure).

3.1 Certificado del servidor

3.1.1 Configuración del webservice

Antes que nada, se configura el webservice para que haga uso de su certificado. El certificado generado por GlassFish al momento de instalarlo se encuentra en el almacén de claves config/keystore.jks dentro del directorio del dominio usado. Para hacer que el webservice sea accesible solamente mediante https usando este certificado debemos:

  • Crear dentro del proyecto WebServiceSimple un nuevo "GlassFish Deployment Descriptor", con los valores por defecto.

  • En el archivo creado (que es un XML llamado sun-ejb-jar.xml dentro de Configuration Files), agregar un nuevo puerto y setear "transport-guarantee" con el valor "CONFIDENTIAL". Debería quedar de la siguiente forma3:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sun-ejb-jar PUBLIC "-//Sun Microsystems, Inc.//DTD Application Server 9.0 EJB 3.0//EN" "http://www.sun.com/software/appserver/dtds/sun-ejb-jar_3_0-0.dtd">
<sun-ejb-jar>
  <enterprise-beans>
    <ejb>
      <ejb-name>ServicioSimple</ejb-name>
      <webservice-endpoint>
        <port-component-name>ServicioSimple</port-component-name>
        <transport-guarantee>CONFIDENTIAL</transport-guarantee>
      </webservice-endpoint>
    </ejb>
  </enterprise-beans>
</sun-ejb-jar>

3.1.2 Configuración del cliente

Si en este momento probamos correr el cliente del webservice, nos da un error "Premature end of file" y en el servidor GlassFish el error es "Invalid request scheme for Endpoint ServicioSimple. Expected https . Received http", lo que parece bastante lógico. Lo primero que hay que hacer es que el cliente acceda a la nueva dirección del webservice. La forma más fácil de lograrlo es borrando el cliente del webservice dentro del proyecto, y recreándolo. Para ello:

  • Dentro del proyecto cliente, borrar "ServicioSimple" dentro de "Web Service References".

  • Crear un nuevo cliente del webservice, como al principio (click derecho sobre el proyecto, "New" - "Web Service Client"), pero esta vez ingresar la nueva dirección del WSDL.

  • En este momento el IDE nos pregunta si deseamos aceptar el certificado de GlassFish, lo que lógicamente tenemos que hacer.

  • Recompilamos el proyecto cliente.

Ahora el proyecto cliente está listo para acceder al webservice mediante https. Sin embargo, si lo corremos da el error "unable to find valid certification path to requested target", lo que también es lógico, ya que el cliente desconoce el certificado del servidor, y no tiene forma de validarlo. Vale la apena aclarar que cuando recreamos el cliente del webservice y aceptamos el certificado, se acepta solamente para el uso del IDE (para generar las clases del cliente del webservice), y no para la ejecución del proyecto. Lo que hay que hacer entonces, es copiar el certificado del servidor, a un keystore de confianza de la aplicación cliente. Para ello:

  • Desde una consola, estando en el directorio config dentro del directorio del dominio de GlassFish, exportar el certificado del servidor a un archivo (el alias del certificado creado por defecto en GlassFish es s1as):

keytool -keystore keystore.jks -exportcert -alias s1as -rfc -file certificadoServidor

Este comando pedirá la contraseña del keystore, que sino fue cambiada debería ser "changeit", y luego exportará el certificado al archivo certificadoServidor.

  • Moverse a otro directorio, que será donde se crearán los almacenes de claves para la aplicación cliente; mover el archivo certificadoServidor a este lugar también. Desde allí, crear un nuevo almacén de claves para el cliente (llamado almacenConfianza.jks), que será el almacén en donde estén los certificados en que el cliente confía, importando el certificado del servidor:

keytool -import -v -alias s1as -file certificadoServidor -keystore almacenConfianza.jks 

En este momento nos pedirá una contraseña para el nuevo almacén, y luego de ingresarla y repetirla, preguntará si confiar o no en el certificado importado, a lo se le debe decir que si.

  • Ahora se debe configurar en el proyecto cliente el almacén de confianza, así como su contraseña, para ello:

    • Click derecho en el proyecto, "Properties" – "Run".

    • En la opción "VM Options" ingresar lo siguiente:

-Djavax.net.ssl.trustStore="$DIR_ALMACENES/almacenConfianza.jks" -Djavax.net.ssl.trustStorePassword="passwordDelAlmacen"

Siendo $DIR_ALMACENES el directorio en donde creamos el almacén para el cliente.

En este momento ya se puede recompilar la aplicación cliente, y ejecutarla, lo que debería funcionar sin problemas. Lo que está faltando ahora, son las configuraciones del certificado del cliente.

3.2 Certificado del cliente

En esta subsección se indica como crear y configurar el certificado del cliente para autenticación y autorización. Primero se muestra como configurar el webservice para requerir autenticación de los clientes con certificados, comprobando que luego de esto, el cliente no puede acceder nuevamente al mismo. Luego de esto se indica como crear un nuevo almacén de claves para el cliente (configurándolo en el mismo), junto con un certificado, y como copiar este último al almacén de confianza del servidor. Finalmente se describe como hacer para que la identificación que contiene el certificado del cliente, también sirva para autorizar o no su acceso a determinados recursos.

3.2.1 Configuración del webservice para autenticación del cliente con certificado

  • Modificar el archivo sun-ejb-jar.xml dejándolo de la siguiente manera (las líneas en gris son las agregadas):

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sun-ejb-jar PUBLIC "-//Sun Microsystems, Inc.//DTD Application Server 9.0 EJB 3.0//EN" "http://www.sun.com/software/appserver/dtds/sun-ejb-jar_3_0-0.dtd">
<sun-ejb-jar>
  <enterprise-beans>
    <ejb>
      <ejb-name>ServicioSimple</ejb-name>
      <webservice-endpoint>
        <port-component-name>ServicioSimple</port-component-name>
        <login-config>
          <auth-method>CLIENT-CERT</auth-method>
          <realm>certificate</realm>
        </login-config>
        <transport-guarantee>CONFIDENTIAL</transport-guarantee>
      </webservice-endpoint>
    </ejb>
  </enterprise-beans>
</sun-ejb-jar>
  • Después hacer nuevamente un deploy al webservice, y ejecutar el cliente. El error del lado del servidor es algo así como "CLIENT CERT authentication error for ServicioSimple".

3.2.2 Creación y configuración del certificado del cliente

  • Crear un nuevo almacén de claves (almacen.jks) con un nuevo certificado del cliente (de alias cliente). Desde una consola en $DIR_ALMACENES ejecutar:

keytool -genkey -alias cliente -keystore almacen.jks

El comando pregunta una clave para el nuevo almacén, y los datos para el nuevo certificado. Para el ejemplo se usó:

    • "first and last name": nombre

    • "organizational unit": informatica

    • "organization": org

    • "City or Locality": ciudad

    • "State or Province": estado

    • "two-letter country code for this unit": np

Luego pide confirmación de que el identificador (CN=nombre, OU=informatica, O=org, L=ciudad, ST=estado, C=np) es correcto, y pregunta por una contraseña para el certificado, la cual no se ingresó tomándose la misma que el almacén de datos.

  • Configurar el nuevo almacén de datos en el proyecto cliente. En el mismo lugar que se agregaron los parámetros para el almacén de claves de confianza, agregar:

-Djavax.net.ssl.keyStore="$DIR_ALMACENES/almacen.jks" -Djavax.net.ssl.keyStorePassword="passwordDelAlmacen"
  • Con el comando keytool exportar el certificado recién creado a un archivo:

keytool -keystore almacen.jks -exportcert -alias cliente -rfc -file certificadoCliente
  • Moverse en la consola hasta el directorio config dentro del dominio de GlassFish, junto con el archivo certificadoCliente. Importar este al almacén de claves de confianza del servidor GlassFish:

keytool -importcert -alias cliente -file certificadoCliente -keystore cacerts.jks
  • Reiniciar el servidor GlassFish, porque no toma el nuevo certificado inmediatamente.

  • En este momento, el cliente ya puede acceder nuevamente al webservice sin problemas, habiendo autenticación bi-direccional con certificados.

3.2.3 Autorización

Se llegó a un punto en que la autenticación bi-direccional con certificados está resuelta. Es momento de mostrar como usar la identificación del certificado cliente, también para la autorización de acceso al webservice. Para ello:

  • Definir el rol rolPrueba en el archivo sun-ejb-jar.xml. Quedaría como sigue:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sun-ejb-jar PUBLIC "-//Sun Microsystems, Inc.//DTD Application Server 9.0 EJB 3.0//EN" "http://www.sun.com/software/appserver/dtds/sun-ejb-jar_3_0-0.dtd">
<sun-ejb-jar>
  <security-role-mapping>
    <role-name>rolPrueba</role-name>
  </security-role-mapping>
  <enterprise-beans>
    <ejb>
      <ejb-name>ServicioSimple</ejb-name>
      <webservice-endpoint>
        <port-component-name>ServicioSimple</port-component-name>
        <login-config>
          <auth-method>CLIENT-CERT</auth-method>
          <realm>certificate</realm>
        </login-config>
        <transport-guarantee>CONFIDENTIAL</transport-guarantee>
      </webservice-endpoint>
    </ejb>
  </enterprise-beans>
</sun-ejb-jar>
  • Anotar el método metodoSimple del webservice, para que solamente el rol rolPrueba tenga autorización. Esto se logra con la anotación @RolesAllowed({"rolPrueba"}).

  • Hacerle un deploy nuevamente al webservice. Si en este momento se ejecuta el cliente, da el error "Client not authorized for invocation of public java.lang.String prueba.ServicioSimple.metodoSimple (java.lang.String)".

  • Lo que hay que hacer entonces, es asociar al identificador del certificado del cliente, con el rol rolPrueba. Esto se logra agregando una línea al archivo sun-ejb-jar.xml, que quedaría como sigue:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sun-ejb-jar PUBLIC "-//Sun Microsystems, Inc.//DTD Application Server 9.0 EJB 3.0//EN" "http://www.sun.com/software/appserver/dtds/sun-ejb-jar_3_0-0.dtd">
<sun-ejb-jar>
  <security-role-mapping>
    <role-name>rolPrueba</role-name>
    <principal-name>CN=nombre, OU=informatica, O=org, L=ciudad, ST=estado, C=np</principal-name>
  </security-role-mapping>
  <enterprise-beans>
    <ejb>
      <ejb-name>ServicioSimple</ejb-name>
      <webservice-endpoint>
        <port-component-name>ServicioSimple</port-component-name>
        <login-config>
          <auth-method>CLIENT-CERT</auth-method>
          <realm>certificate</realm>
        </login-config>
        <transport-guarantee>CONFIDENTIAL</transport-guarantee>
      </webservice-endpoint>
    </ejb>
  </enterprise-beans>
</sun-ejb-jar>

Por supuesto, que se podrían agregar tantos elementos principal-name como fuera necesario.

  • En este momento, y haciendo un deploy nuevamente del proyecto, se puede comprobar que el cliente nuevamente puede acceder sin problemas al webservice.

  • También se podría obtener la identificación del certificado del cliente desde dentro del código del webservice, para realizar determinadas tareas dependiendo de esta. Esto se logra obteniendo los objetos de clase java.security.Principal del "usuario" autenticado4, ya que uno de estos objetos corresponde al certificado. En la construcción de este ejemplo, solo queda asociado un objeto de clase Principal, que es el del certificado. En general se podrían ver todos estos objetos de la siguiente forma:

Subject subject = (Subject) PolicyContext.getContext("javax.security.auth.Subject.container");
for (Principal p : subject.getPrincipals()) {
   System.out.println("Principal del usuario: '" + p.getName() + "'.");
}

Probablemente si hubiese más de un objeto Principal se podría identificar el que corresponde al certificado, por medio del método getClass de esta clase.


1El idioma del software usado para las pruebas, fue el inglés.

2Lo que hace el IDE en este caso, es crear un EJB y anotarlo con @WebService().

3En NetBeans, este archivo se puede editar como un XML corriente, o con una interfase gráfica que provee el IDE.

4Se escribe "usuario" entre comillas, porque lo que se autentica puede que no sea una persona. Por ejemplo, el uso de certificados es una buena forma para que sistemas y no personas se autentiquen entre sí.

10 comentarios:

  1. Buenos días.... llevo 8 meses en este problema de certificados y SSL para web services y he leído esta guía y la pondré en practica, espero seas mi salvación!
    Un Saludo

    ResponderBorrar
  2. Respuestas
    1. En mi trabajo, lo hemos está usando por más de un año. Claro que funciona; tal vez hay algunos detalles distintos para versiones de GlassFish más nuevas, pero funciona.

      Borrar
  3. saludos,

    resulta que cuando compilo el cliente me salen los siguientes warnings:
    "[WARNING] schema_reference.4: Fallo al leer el documento de esquema 'https://pcuvmsss:8181/ServicioSimple/ServicioSimple?xsd=1', porque 1) no se ha encontrado el documento; 2) no se ha podido leer el documento; 3) el elemento raíz del documento no es ."

    y cuando ejecuto el cliente:
    Exception in thread "main" javax.xml.ws.WebServiceException: Cannot find 'https://host:8181/ServicioSimple/ServicioSimple?wsdl' wsdl. Place the resource correctly in the classpath.

    He depurado y veo que le es imposible alcanzar el recurso, aunque pegando la misma URL en el navegador si obtengo el WSDL. ¿Qué puedo hacer?

    Muchas gracias

    ResponderBorrar
    Respuestas
    1. perdón, utilizo:
      IDE: NetBeans IDE 7.1
      Servidor de aplicaciones: GlassFish Server Open Source Edition 3.1.1 (build 12)
      OS: Windows 7
      Java: 1.7.2

      Borrar
    2. Es medio tarde para contestarte, pero tal vez le sirve a alguien más. Yo también tuve un problema parecido en una versión más nueva que la de este documento, y la única solución que encontré fue la que se ve en http://forum.bdp.betfair.com/showpost.php?p=3528&postcount=4

      Borrar
  4. BIEN EXPLICADO BUENA. AMIGO. ME AYUDO MUCHO

    ResponderBorrar
  5. Buenas, muchas gracias por publicar este blob de verdad me ayudo mucho y constato que funciona, ahora tengo otro problema y quisiera consultarles, al momento de realizar la autentificacion por medio de un proxy reverso en apache no me permite estableser la conexion, alguna sugerencia de como realizar la configuración en el proxy para solucionarlo, saludos.

    ResponderBorrar
    Respuestas
    1. Una conexión https no puede pasar por un proxy reverso, por su propia naturaleza. Creo que hay dos opciones. La primera es simplemente no usar el proxy reverso, y usar reglas de firewall para redirigir las conexiones al servidor GlassFish. La segunda es que los clientes se conecten mediante https con el proxy reverso, y este se conecte con el servidor GlassFish, partiendo de esta forma la conexión en dos. Esto último nunca lo probé en GlassFish, y no se si se puede hacer, pero si lo he probado en JBoss, y de hecho es una arquitectura bastante usada.

      Borrar
  6. Estoy usando la opcion SSLOptions +ExportCertData en el proxy pero tambien me arroja error.

    ResponderBorrar