====== 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:
{{ :fdw2.0:fundeweb2.0:gt:jsonwebtokendetalle.jpg |}}
* El primer módulo es el que se denomina **JOSE** o JavaScript Object Signing and Encryption y define cual es la tecnología criptográfica que se va a aplicar al token para securizar la información.
* El segundo módulo es el JWT PayLoad o **JWT Claims** y almacena la información de negocio que necesitamos en el token.
* El tercer módulo es la **firma** JWT que se encarga de dar validez al token.
===== 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 ===
* Abrimos el POM principal del proyecto y actualizamos la versión del //// a 2.0.31 (2.0.31-primefaces6 para aplicaicones con PrimeFaces 6).
* Abrimos el POM del módulo Web y añadimos despues de la configuración de las librerias de Jersey (buscamos el texto ////).
...
org.bitbucket.b_c
jose4j
* Abrimos el fichero //weblogic.xml// y añadimos la siguiente configuración:
jose4j
0
WAR
true
=== Aplicación FundeWeb 1.5 ===
* Abrimos el POM principal del proyecto y actualizamos la versión del //// a 1.5.29.
* Abrimos el fichero //weblogic-application.xml// y añadimos la siguiente configuración:
jose4j
0
EAR
true
* Abrimos el POM del módulo EJB y añadimos despues de la configuración de las librerias de Jersey (buscamos el texto ////).
...
org.bitbucket.b_c
jose4j
=== 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 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 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 roles = new ArrayList();
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 entity = new GenericEntity( error ) {};
return this.makeCORS( Response.status( 400 ).entity( entity ) );
}
if ( password == null ) {
RespuestaError error = new RespuestaError( "Falta la contraseña" );
GenericEntity entity = new GenericEntity( 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 entity = new GenericEntity( 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();
}