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.
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:
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.
Para poder dotar a nuestro proyecto de la capacidad de generar tokens y validarlos deberemos hacer lo siguiente
Añadir dependencia con librería jose4j en el pom.xml del módulo web
<dependency> <groupId>org.bitbucket.b_c</groupId> <artifactId>jose4j</artifactId> <version>0.5.5</version> </dependency>
Incluir la clase FundewebRestJWTBean que será la encargada de codificar y comprobar el token
package es.um.atica.restSeguro.backbeans; import static org.jboss.seam.annotations.Install.FRAMEWORK; import java.util.ArrayList; import java.util.List; import org.jboss.seam.ScopeType; 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 = FRAMEWORK ) @BypassInterceptors public class FundewebRestJWTBean { @Logger Log log; public final static String ALGORITMO_CIFRADO = AlgorithmIdentifiers.RSA_USING_SHA256; private final String ID_CLAVE_CIFRADO = "fundewebJWTKey"; private RsaJsonWebKey cifrador; public void inicializaCifrador() { try { cifrador = RsaJwkGenerator.generateJwk( 2048 ); cifrador.setKeyId( ID_CLAVE_CIFRADO ); } catch ( JoseException e ) { e.printStackTrace(); } } /** * 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() { // El cifrador es nulo en cada consulta, ver por que if ( cifrador == null ) { inicializaCifrador(); } 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(); }