Está en la página 1de 6

Autenticación con JWT (JSON Web Token)

Para asegurar nuestros servicios REST implementados con JAX-RS es necesario utilizar
texto cifrado (tokens) utilizando el estándar JWT. JWT (JSON Web Token) define una
forma compacta y autónoma para transmitir información de forma segura entre las partes
como un objeto JSON, esta información se puede verificar y confiar porque está firmada
digitalmente. Los JWT se pueden firmar usando una clave (con el algoritmo HMAC ) o un
par de claves pública/privada usando el sistema criptográfico RSA, iniciales de los tres
creadores (Rivest, Shamir y Adleman) o el algoritmo de firma digital de curva elíptica
(ECDSA, Elliptic Curve Digital Signature Algorithm).

En la autenticación, cuando el usuario inicia sesión usando sus credenciales, se devolverá


un JSON token web y se guardará localmente, en lugar del enfoque de crear una sesión
en el servidor y devolver una cookie. Siempre que el usuario desee acceder a una ruta
protegida, debe enviar el JWT, generalmente en el encabezado con el nombre
Authorization, este es un mecanismo de autenticación sin estado ya que el estado del
usuario nunca se guarda en la memoria del servidor. Las rutas protegidas del servidor
verifican si hay un JWT válido en el encabezado de Authorization, y si es que los tiene, el
usuario será permitido el acceso al recurso solicitado. Como los JWT son autónomos, toda
la información necesaria está descrito dentro del token, lo que reduce la necesidad de
hacer las consultas en cada momento a la base de datos.

La estructura de JWT consta de tres partes separadas por puntos «.» que son:
- Encabezado (Header). Es una cadena JSON cifrada en Base64 que contiene el tipo de
token y la codificación utilizada para la firma.
Header = '{"alg": "HS256", "typ": "JWT"}'
- Cuerpo del mensaje (Payload). Es un conjunto de información en formato JSON que
contiene las peticiones (claims), que son un conjunto de campos predefinidos, así como
algunos propios que podemos añadir, como los roles, este mensaje también es cifrada
en Base64.
Payload = '{"sub":"ichagua","name":"Irenio Luis",
"roles":"admin "}'
- Firma (Signature). Está formado por el encabezado y el cuerpo del mensaje, encriptado
con una clave secreta. Esta parte es la que garantiza la integridad de los datos, es decir,
que los datos anteriores no han sido alterados.
clave = 'secretkey'
unsignedToken = encodeBase64Url (encabezado) + '.' +
EncodeBase64Url (mensaje)
signature = HMAC-SHA256 (clave, unsignedToken)

El resultado son tres cadenas de URL Base64 separadas por puntos que se pueden pasar
fácilmente en entornos HTML y HTTP, a la vez que son más compactas en comparación con
los estándares basados en XML. Se muestra un JWT que tiene el encabezado anterior y el
mensaje del cuerpo codificado, y está firmado con una clave.

eyJhbGciOiJIUzUxMiJ9.
eyJyb2xlcyI6IkFETUlOSVNUUkFET1IiLCJzdWIiOiJhZG1pbiIsImlzcyI6Imh0dHA6Ly93d
3cubnNwc2FjLmNvbSIsImlhdCI6MTU5MDMyMzc1NSwiZXhwIjoxNTkwMzI1NTU1fQ.
AUl6mGs__Zj9SlVZLuC_DnDDEu8loO4Bsoi6d2mBTtFG1ByuuzKn-
ElW6vo7wCS_B7kv_Cs3H-JCUeS_w0GD9w

Este token cifrado es la que senviará en el encabezado del mensaje en los accesos a
recursos protegidos. Para generar este token y enviar los mensajes utilizaremos la librería
JJWT, que implementa el estándar JWT, para esto agregamos las dependencias en el
proyecto Maven de nuestra aplicación RESTful AplicacionREST, el código será el siguiente.

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>org.codehaus.jettison</groupId>
<artifactId>jettison</artifactId>
<version>1.4.1</version>
</dependency>

El filtro que habíamos creado para la autenticación básica, lo tenemos que reescribir, ya
que ahora interceptará los mensajes con el cifrado de token JWT. Para esto modificamos
la clase AuthenticationFilter y el código será el siguiente.

package com.nspsac.academico.filter;

import java.io.IOException;
import java.lang.reflect.Method;
import java.security.Key;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.annotation.security.DenyAll;
import javax.annotation.security.PermitAll;
import javax.annotation.security.RolesAllowed;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.ResourceInfo;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import io.jsonwebtoken.ClaimJwtException;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.impl.crypto.MacProvider;

import com.nspsac.academico.service.ResponseBuilder;

public class AuthenticationFilter implements ContainerRequestFilter {


public static final Key KEY = MacProvider.generateKey();
public static final String AUTHORIZATION_PROPERTY = "x-access-token";

private static final String ACCESS_INVALID_TOKEN =


"Token invalid. Please authenticate again!";
private static final String ACCESS_DENIED =
"Not allowed to access this resource!";
private static final String ACCESS_FORBIDDEN = "Access forbidden!";

@Context
private ResourceInfo resourceInfo;

@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
Method method = resourceInfo.getResourceMethod();
if (!method.isAnnotationPresent(PermitAll.class)) {
if (method.isAnnotationPresent(DenyAll.class)) {
requestContext.abortWith(
ResponseBuilder.create(Response.Status.FORBIDDEN,ACCESS_FORBIDDEN)
);
return;
}

final MultivaluedMap<String, String> headers =


requestContext.getHeaders();
final List<String> authProperty = headers.get(AUTHORIZATION_PROPERTY);
if( authProperty == null || authProperty.isEmpty() ) {
requestContext.abortWith(
ResponseBuilder.create(Response.Status.UNAUTHORIZED,ACCESS_DENIED)
);
return;
}
String token = authProperty.get(0);
Jws<Claims> claims = null;
try {
claims = Jwts.parser().setSigningKey(KEY).parseClaimsJws(token);
} catch ( ClaimJwtException | SignatureException e ) {
HashMap<String, Object> hmMsg = new HashMap<String, Object>();
hmMsg.put("message", ACCESS_INVALID_TOKEN);
hmMsg.put("description", e);
requestContext.abortWith(
ResponseBuilder.create(Response.Status.UNAUTHORIZED, hmMsg)
);
return;
}

String roles = (String) claims.getBody().get("roles");


if (method.isAnnotationPresent(RolesAllowed.class)) {
RolesAllowed rolesAnnotation =
method.getAnnotation(RolesAllowed.class);
Set<String> rolesSet =
new HashSet<String>(Arrays.asList(rolesAnnotation.value()));
if (!isUserAllowed(roles, rolesSet)) {
requestContext.abortWith(
ResponseBuilder.create(Response.Status.UNAUTHORIZED,ACCESS_DENIED)
);
return;
}
}
}
}
private boolean isUserAllowed(final String userRole, final Set<String> rolesSet) {
boolean isAllowed = false;
if (rolesSet.contains(userRole)) {
isAllowed = true;
}
return isAllowed;
}
}

Para generar el token hemos utilizado la clase MacProvider de la propia libreria JWT. El
método usado genera una clave secreta aleatoria de 512 bits. Podría utilizarse también
alguna clave almacenada en un keystore. En el mensaje Jws<Claims> claims se tiene los
atributos definidos en las credenciales de acceso. Para varios casos que se presente en la
interceptación del mensaje, se ha creado un mensaje personalizado y de diferentes formas
de enviar un mensaje de tipo JSON, le hemos dado el nombre de la clase ResponseBuilder
que tiene el siguiente código.
package com.nspsac.academico.service;

import java.util.List;
import java.util.Map;
import javax.ws.rs.core.Response;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import com.fasterxml.jackson.databind.JsonSerializable;

public class ResponseBuilder {


public static Response create(Response.Status status) {
JSONObject jsonObject = new JSONObject();
try {
jsonObject.put("message", status.toString());
} catch (JSONException e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Response.Status.INTERNAL_SERVER_ERROR).build();
}
return Response.status(status).entity(jsonObject.toString()).build();
}
public static Response create(Response.Status status, String message) {
JSONObject jsonObject = new JSONObject();
try {
jsonObject.put("message", message);
} catch (JSONException e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Response.Status.INTERNAL_SERVER_ERROR).build();
}
return Response.status(status).entity(jsonObject.toString()).build();
}
public static Response create(
Response.Status status, JsonSerializable json)
throws JSONException {
return Response.status(status).entity(json.toString()).build();
}
public static Response create(
Response.Status status, List<JsonSerializable> json)
throws JSONException {
JSONArray jsonArray = new JSONArray();
for (int i = 0; i < json.size(); i++) {
jsonArray.put(json.get(i));
}
return Response.status(status).entity(jsonArray.toString()).build();
}
public static Response create(
Response.Status status, Map<String, Object> map) {
JSONObject jsonObject = new JSONObject();
try {
for (Map.Entry<String, Object> entry : map.entrySet()) {
jsonObject.put(entry.getKey(), entry.getValue());
}
} catch (JSONException e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Response.Status.INTERNAL_SERVER_ERROR).build();
}
return Response.status(status).entity(jsonObject.toString()).build();
}
}

En el filtro de autenticación creado se descifra las credenciales de acceso, para enviar el


token cifrado tenemos que generar una clase, para esto creamos la clase UsuarioService
dentro del paquete com.nspsac.academico.service y el código será el siguiente para
generar un token para autenticar un usuario.

package com.nspsac.academico.service;

import java.util.Base64;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.StringTokenizer;

import javax.annotation.security.PermitAll;
import javax.naming.AuthenticationException;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import com.nspsac.academico.filter.AuthenticationFilter;

@Path("/usuarios")
public class UsuarioService {

@PermitAll
@GET
@Path("/login")
@Produces("application/json;charset=UTF-8")
public Response authenticateUser(
@HeaderParam("Authorization") String tokenAuth) {
try {
final String encodedUserPasswd = tokenAuth.replaceFirst("Basic ", "");
byte[] decodedBytes = Base64.getDecoder().decode(encodedUserPasswd);
String userAndPasswd = new String(decodedBytes, "UTF-8");
StringTokenizer tokenizer = new StringTokenizer(userAndPasswd, ":");
String username = tokenizer.nextToken();
String password = tokenizer.nextToken();
String roles = authenticate(username, password);
String token = issueToken(username, roles);
System.out.println("token:"+token);
HashMap<String, Object> map = new HashMap<String,Object>();
map.put( AuthenticationFilter.AUTHORIZATION_PROPERTY, token );
return Response.ok().header(HttpHeaders.AUTHORIZATION, map).build();
} catch (Exception e) {
return Response.status(Response.Status.CONFLICT).build();
}
}
private String issueToken(String username, String roles) {
Date issueDate = new Date();
Calendar calendar = Calendar.getInstance();
calendar.setTime(issueDate);
calendar.add(Calendar.MINUTE, 30);
Date expireDate = calendar.getTime();

//Creamos el token
String jwtToken = Jwts.builder()
.claim("roles", roles)
.setSubject(username)
.setIssuer("http://www.nspsac.com")
.setIssuedAt(issueDate)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS512, AuthenticationFilter.KEY)
.compact();
return jwtToken;
}
private String authenticate(String user, String password)
throws AuthenticationException{
if("test".equals(user) && "test".equals(password)) {
return "COORDINADOR";
} else if("admin".equals(user) && "admin".equals(password)) {
return "ADMINISTRADOR";
} else {
throw new AuthenticationException();
}
}
}

En este servicio se tiene que invocar a la URI /s/usuarios/login, en este recurso se tiene
que enviar las credenciales de acceso con la autenticación básica en el encabezado del
mensaje, tratado en esta misma sección, se obtiene los roles de este usuario, en este
código se hace una validación simple, hemos indicado que debería ser desde una base de
datos, luego se genera el token del cuerpo del mensaje (claim), al generar el token se le
da los valores a los atributos, como nombre de usuario, roles, la fecha de creación, el
tiempo de expiración del token de 30 minutos y el cifrado de la firma digital, al invocar a
este servicio se tiene el token generado en el encabezado del mensaje, tal como se
muestra en la Figura 5.13.

Figura 5.1. Generación de token JWT

El token generado se le asigna en el encabezado del mensaje a solicitar, para invocar al


servicio de obtener la lista de estudiantes se tenía la URI /s/estudiantes, a este servicio se
le asigna un encabezado con nombre «x-access-token» y el valor será el token generado, y
si el token es válido mostrará el resultado esperado, tal como se muestra en la Figura 5.14.

Figura 5.2. Mensaje con token JWT

También podría gustarte