Tabla de Contenidos

Creación de servicios REST seguros con JWT

En esta sección de la wiki vamos a explicar cómo podemos crear servicios rest que vayan protegidos con JWT: JSON Web Token. Este mecanismo nos permite enviar un token en vez del usuario y contraseña en cada petición que realicemos. De igual manera nos permite codificar en dicho token cierta metainformación sobre el usuario y la comunicación que se está realizando.

¿Qué es JSON Web Token (JWT)?

Un JSON Web Token es,básicamente, un contenedor de información referente a la autenticación de un usuario. Esta información se codifica en tres módulos diferentes separados por “.” y cifrados.

El contenido de los módulos es el siguiente:

Crear un servicio REST securizado

Los servicios REST pueden requerir autenticación de usuarios para acceder a determinada funcionalidad, para ello deberemos hacer que el usuario haga login en nuestra aplicación. Tras eso generaremos un token que será lo que el usuario de nuestro servicio deberá mandar en el resto de peticiones. Es responsabilidad nuestra recuperar y validar el token.

Como recomendación, los servicios públicos podemos exponernos en el path raiz:

 http://miaplicacion.um.es/miaplicacion/rest/servicioX

y los servicios que requieran autenticación incluirlos en un paquete “auth”:

 http://miaplicacion.um.es/miaplicacion/rest/auth/servicioSeguroX

Para poder realizar el login del usuario deberemos tener, en nuestro servicio REST un método con esta signatura

@Path( "/auth/login" )
@GET
@Produces( MediaType.APPLICATION_JSON )
public Response autenticaUsuario( @HeaderParam( "user" ) String usuario, @HeaderParam( "pass" ) String password,@HeaderParam("aplicacion") String aplicacion )throws JsonGenerationException,

Los parámetros que recibe son HeaderParam, es decir, van en la cabecera de la petición y no en el cuerpo, esta forma de enviarlos es una recomendación para mantener un alto nivel de seguridad en la comunicación.

A este método hay que pasarle el usuario, la contraseña y la aplicación que accede al servicio.

Configuración del Proyecto

Como requisito, tenemos que tener instaldo el parche 2.0.45 de FundeWeb.

Para poder dotar a nuestro proyecto de la capacidad de generar tokens y validarlos deberemos hacer lo siguiente:

Aplicación FundeWeb 2.0

		...
		<!-- FIN - Servicios Rest / Jersey / Jackson -->
 
		<!-- JWT -->
		<dependency>
		   <groupId>org.bitbucket.b_c</groupId>
		    <artifactId>jose4j</artifactId>
		</dependency>
		<!-- FIN - JWT -->
    <library-ref>
        <library-name>jose4j</library-name>
        <specification-version>0</specification-version>
        <implementation-version>WAR</implementation-version>
        <exact-match>true</exact-match>
    </library-ref>

Aplicación FundeWeb 1.5

    <library-ref>
        <library-name>jose4j</library-name>
        <specification-version>0</specification-version>
        <implementation-version>EAR</implementation-version>
        <exact-match>true</exact-match>
    </library-ref>
		...
		<!-- Fin Servicios Web CXF -->
 
		<!-- JWT -->
		<dependency>
		   <groupId>org.bitbucket.b_c</groupId>
		    <artifactId>jose4j</artifactId>
		</dependency>
		<!-- FIN - JWT -->

Añadimos el Componente FundewebRestJWTBean

Incluir la clase FundewebRestJWTBean que será la encargada de codificar y comprobar el token

package es.um.atica.restSeguro.backbeans;
 
import java.util.ArrayList;
import java.util.List;
 
import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.Create;
import org.jboss.seam.annotations.Install;
import org.jboss.seam.annotations.Logger;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Scope;
import org.jboss.seam.annotations.Startup;
import org.jboss.seam.annotations.intercept.BypassInterceptors;
import org.jboss.seam.log.Log;
import org.jose4j.jwa.AlgorithmConstraints;
import org.jose4j.jwa.AlgorithmConstraints.ConstraintType;
import org.jose4j.jwk.RsaJsonWebKey;
import org.jose4j.jwk.RsaJwkGenerator;
import org.jose4j.jws.AlgorithmIdentifiers;
import org.jose4j.jws.JsonWebSignature;
import org.jose4j.jwt.JwtClaims;
import org.jose4j.jwt.MalformedClaimException;
import org.jose4j.jwt.consumer.InvalidJwtException;
import org.jose4j.jwt.consumer.JwtConsumer;
import org.jose4j.jwt.consumer.JwtConsumerBuilder;
import org.jose4j.lang.JoseException;
 
@Startup
@Scope( ScopeType.APPLICATION )
@Name( "fundewebRestJWTBean" )
@Install( precedence = Install.FRAMEWORK )
@BypassInterceptors
public class FundewebRestJWTBean {
 
    @Logger
    private Log log;
 
    public final static String ALGORITMO_CIFRADO = AlgorithmIdentifiers.RSA_USING_SHA256;
    private final String ID_CLAVE_CIFRADO = "fundewebJWTKey";
 
    private RsaJsonWebKey cifrador;
 
    @Create
    public void inicializaCifrador() {
        try {
            cifrador = RsaJwkGenerator.generateJwk( 2048 );
            cifrador.setKeyId( ID_CLAVE_CIFRADO );
        }
        catch ( JoseException e ) {
            log.error("Error al iniciar el cifrador.", e);
        }
    }
 
    /**
     * Genera el token JWT para el usuario validado
     * @param emisor quien emite el token
     * @param aplicacion quien accede al servicio
     * @param roles lista de roles calculada para el usuario concreto
     * @return devuelve un token JWT o nulo si hay algun error
     */
    public String generaTokenCifrado(String emisor, String aplicacion,List<String> roles) {
        JwtClaims claims = new JwtClaims();
        // La aplicación que genera el token
        claims.setIssuer( emisor );
        // Cuando expirará el token (10 minutos)
        claims.setExpirationTimeMinutesInTheFuture( 10 );
        // Para quien voy a generar el token
        claims.setAudience( aplicacion );
        // Identificador unico para el token
        claims.setGeneratedJwtId();
        // El token ha sido creado ahora
        claims.setIssuedAtToNow();
        // Tiempo en el pasado por el que el token no puede ser valido (2 minutos)
        claims.setNotBeforeMinutesInThePast( 2 );        
        // Lista de roles a los que se aplica el token
        claims.setStringListClaim( "roles", roles );
 
        JsonWebSignature jws = new JsonWebSignature();
        // Contenido del token en formato JSON
        jws.setPayload( claims.toJson() );
        // Id de la clave de cifrado
        jws.setKeyIdHeaderValue( getCifrador().getKeyId() );
        // Clave de cifrado
        jws.setKey( getCifrador().getPrivateKey() );
 
        // Seleccion de algoritmo de cifrado
        jws.setAlgorithmHeaderValue( FundewebRestJWTBean.ALGORITMO_CIFRADO );
        String jwt = null;
        try {
            // Generacion de token compactado
            jwt = jws.getCompactSerialization();
        }
        catch ( JoseException e ) {
            log.error( "Error generando token JWT", e );
            return null;
        }
        return jwt;
    }
 
    /**
     * Valida el token recibido y devuelve la lista de roles para el usuario
     * @param token: token JWT
     * @param emisor: emisor del token
     * @param aplicacion: aplicacion que accede al servicio
     * @return La lista de roles si el token es valido o InvalidJwtException si el token no es valido
     * @throws InvalidJwtException
     */
    public List<String> validaToken(String token, String emisor, String aplicacion) throws InvalidJwtException{
        JwtConsumer jwtConsumer = new JwtConsumerBuilder()
        //El token debe tener fecha de expiracion
        .setRequireExpirationTime() 
        //Permitir un intervalo de segundos de margen a la hora de validar el tiempo de expiracion
        .setAllowedClockSkewInSeconds(30)         
        //El token debe llevar como aplicacion la nuestra
        .setExpectedIssuer(emisor) 
        //Debe llevar como destinatario quien indique en la generacion del token
        .setExpectedAudience(aplicacion)
        //Recupero la clave del cifrador
        .setVerificationKey(getCifrador().getKey())
        //Confirmo que el algoritmo de cifrado es el indicado
        .setJwsAlgorithmConstraints(     new AlgorithmConstraints(ConstraintType.WHITELIST, 
                        AlgorithmIdentifiers.RSA_USING_SHA256)).build();
 
        //Compruebo el token
        JwtClaims jwtClaims = jwtConsumer.processToClaims( token ); 
 
        List<String> roles = new ArrayList<String>();
        try {
            roles = jwtClaims.getStringListClaimValue( "roles" );
        }
        catch ( MalformedClaimException e ) {
            log.error( "Error leyendo roles del token JWT", e );
        }
        return roles;
    }
 
    // --------------- GETTER Y SETTER ------------------------------
 
    public RsaJsonWebKey getCifrador() {
        return cifrador;
    }
 
    public void setCifrador( RsaJsonWebKey cifrador ) {
        this.cifrador = cifrador;
    }
 
}

Recuperarla en nuestro servicio REST

    public FundewebRestJWTBean getFundewebRestJWTBean() {
        if(fundewebRestJWTBean==null){
            fundewebRestJWTBean = (FundewebRestJWTBean)Component.getInstance( "fundewebRestJWTBean",true );
        }
        return fundewebRestJWTBean;
    }

Una vez tengamos acceso a la clase FundewebRestJWTBean deberemos llamar al metodo de generación de token cuando el usuario se autentique y al método de validación de token en cada una de las peticiones que deban ser securizadas.

Un ejemplo de autenticación de usuario

    @GET
    @Produces( MediaType.APPLICATION_JSON )
    public Response autenticaUsuario( @HeaderParam( "user" ) String usuario, @HeaderParam( "pass" ) String password,@HeaderParam("aplicacion") String aplicacion )
    throws JsonGenerationException, JsonMappingException, IOException, JoseException {
        if ( usuario == null ) {
            RespuestaError error = new RespuestaError( "Falta el usuario" );
            GenericEntity<RespuestaError> entity = new GenericEntity<RespuestaError>( error ) {};
            return this.makeCORS( Response.status( 400 ).entity( entity ) );
        }
        if ( password == null ) {
            RespuestaError error = new RespuestaError( "Falta la contraseña" );
            GenericEntity<RespuestaError> entity = new GenericEntity<RespuestaError>( error ) {};
            return this.makeCORS( Response.status( 400 ).entity( entity ) );
        }
 
        // Obtengo el usuario a aprtir del user pass
        if ( !validaUsuario(usuario,password) ) {
            RespuestaError error = new RespuestaError( "Usuario no valido" );
            GenericEntity<RespuestaError> entity = new GenericEntity<RespuestaError>( error ) {};
            return this.makeCORS( Response.status( 400 ).entity( entity ) );
        }
        //Calcular los parametros
        return Response.status( 200 ).entity( getFundewebRestJWTBean().generaTokenCifrado(EMISOR_ID,aplicacion,calculaRoles(usuario)) ).build();
 
    }
 
 
    private Response makeCORS(ResponseBuilder responseBuilder) {
	return responseBuilder.header("Access-Control-Allow-Origin", "*").header("Access-Control-Allow-Methods", "GET, POST, OPTIONS").build();
    }