Guía de Implementación: JWT para la Autenticación en Java

Maria Paula Vizcaíno
Pragma
Published in
5 min readFeb 15, 2024

Constantemente como usuarios de cualquier aplicación en cualquier plataforma, ya sean redes sociales, herramientas laborales hasta juegos online, realizamos procesos de autenticación a los cuales ya estamos acostumbrados; ingresar nuestro usuario que sabemos que es único junto con nuestra contraseña, que muchas veces olvidamos, es un proceso cotidiano.

Siendo la autenticación la puerta para acceder a las aplicaciones, es el punto de inicio en la experiencia del usuario es donde se debe garantizar la seguridad y la protección de su entidad. Un método para asegurar el proceso es el uso de JWT (JSON Web Token), que permite transmitir la información de manera segura, con una firma digital o un cifrado específico entre las partes involucradas en el manejo del JWT.

Este token es compuesto por tres partes: el primero corresponde al encabezado o header el cual contiene metadatos sobre el tipo de token empleado y el algoritmo asociado a la firma o cifrado. Posterior al encabezado, encontramos el payload, esta sección contiene la información que se desea trasmitir al utilizar el token. Luego, se encuentra la firma, la cual valida el origen del token y permite verificar si ha sido modificado; los JWT son tokens autónomos, contando en sí con toda la información necesaria para verificar su validez.

En el proceso de autenticación, una vez el usuario realiza el inicio de sesión proporcionado sus credenciales correctas vinculadas al sistema, el servidor verifica dichas credenciales y retorna un JWT con la información relevante de dicho usuario. Una vez este token se genera, el cliente, ya sea una aplicación web o móvil, lo recibe y lo almacena, generalmente con una cookie del navegador o por medio de almacenamiento local, para garantizar la seguridad del código con HTTPS.

Ahora, por cada solicitud posterior en el flujo del sistema, el cliente incluye el JWT en los headers de autorización, siguiendo el esquema de autenticación Bearer. Este esquema es fácil de implementar y comprender siendo compatibles con arquitecturas escalables y distribuidas, el cual podemos implementar en nuestras aplicaciones Java con la clase Spark.

Cuando el sistema recibe una petición con un JWT procede a validar el proceso de autenticación- Primero, decodifica el token accediendo al encabezado y al payload, luego, verifica la firma del token para cerciorarse de que dicha autenticación no haya sido comprometida y, cuando la firma es validada, se verifica la validez de la información transportada como los permisos del usuario o el tiempo de validez del token. Veamos un ejemplo generalizado para la autenticación:

// Importanos las librerias necesarias para el manejo de JwT
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import spark.Filter;
import spark.Request;
import spark.Response;
import spark.Spark.*;

import java,security.Key;

public class JWTAuthenticationExample

// Establecemos el cifrado para firmar y verificar los JWTs
private static final Key secretKey = Keys.secretKeyFor(io.jsonwebtoken.SignatureAlgorithm.H5256);

public static void main(String[] args) {
// Ruta de inicio de sesión de los pragmaticos para generar un JWT
post ("/login", (req, res) -> {
String pragmatico = req.queryParams("pragmatico");
String password = req.queryParams("password");

// Veríficamos las credeciales del pragmatico de nuestro ejemplo
if ("pragnatico".equals(pragmatico) && "contraseña".equals(password)) {
// Al ser exitosa la autenticación, generamos el JWT
String token =generateToken(pragmatico);
return token;
} else {
// En caso de que las credenciales no correspondan, se le notifica al usuario
res.status(401);
return "Credenciales invalidas";
}
});

// Filtro para verificar la autenticación en todas las solicitudes
before((Filter) (request, response) -> {
String token = request.headers("Authorization");
if (token == null || !token.startsWith("Bearer ")) {
halt(401, "Token de autenticación no proporcionado");
} else {
// Extraemos el JWT sin el prefijo "Bearer "
String jwtToken - token.substring(7);

// Verificamos y decodificamos el JWT
try {
Claims claims = Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(JwtToken)
.getBody();
// Agregamos la información del usuario al objeto de solicitud para uso posterior
request.attribute("user", claims.getsubject());
} catch (Exception e) {
// En caso de que el JWT haya sido comprometido
halt(401, "Token de autenticación inválido");
}
}
});

// Ruta protegida que retorna el nombre de usuario
get("/protected", (req, res) -> {
String username = req.attribute("user");
return "Usuario autenticado: " + username;
});
}

}

Implementación de la autenticación JWT.

Para realizar la implementación de esta autenticación, se requiere, de primera mano, configurar las dependencias específicas. Debemos agregar la dependencia de ‘jjwt’ para Gradle en el archivo build.gradle de la forma:

dependencies {
implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.2'
}

Y para Maven en el archivo pom.xml de la forma:

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>

Una vez agregadas las dependencias debemos crear la clase en la cual validaremos el JWT:

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import java.util.Date;

public class JwtUtil {

private static final Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
// Estableceremos la validez de nuestro token por un lapso de 6 horas (milisegundos)
private static final long expirationTime = 21600000;

public static String generateToken(String subject) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expirationTime);
return Jwts.builder()
.setSubject(subject)
.setIssueAt(now)
.setExpiration(expiryDate)
.signWith(key)
.compact();
}

public static String getSubjectFromToken(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
return claims.getSubject();
}

public static boolean validateToken(String token) {
try {
Jwts.parserBuilder().getSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}

}

Ahora, ya teniendo nuestra clase ‘JwtUtil’ integrémosla en nuestra lógica del proceso de autenticación:

import java.util.HashMap;
import java.util.Map;

public class AuthentiactionService {

private static Map<String, String> pragmaticos = new HashMap<>();

static {
// Agregamos parmáticos a nuestra base de datos
pragmaticos.put("pragmatico1", "contraseña1");
pragmaticos.put("pragmatico2", "contraseña2");
}

public static String login(String pragName, String password) {
if (pragmaticos.containsKey(pragName) && pragmaticos.get(pragName).equals(password)) {
return JwtUtil.generateToken(pragName);
}
return null;
}

public static boolean isAuthenticated(String token) {
return JwtUtil.validateToken(token);
}

}

Por último, por medio de un controlador, verifiquemos la autenticidad de los JWTs:

import spark.Filter;
import spark.Request;
import static spark.Spark.*;

public class ResourceController {

public static void main(String[] args) {
// Filtro para verificar la autenticación en todas las solicitudes
before((Filter) (request, response) -> {
String token = request.headers("Authorization");
if (token == null || !AuthenticationService.isAuthenticated(token)) {
halt(401, "Acceso no autorizado");
}
});
}

}

Porqué emplear JWT.

Una vez especificado su funcionamiento, composición e implementación podemos ver que el uso de este tipo de token le facilita al desarrollador un formato compacto y simple para trasmitir información, brindando eficiencia al sistema que lo implemente. Estos tokens, al ser autónomos, brinda toda la información necesaria para la verificación de su validez, permitiendo reducir dependencias con servicios adicionales de validación y aportando alta escalabilidad.

Podemos firmar los JWTs con el cifrado que deseemos, aunque se recomiendo utilizar algoritmos de cifrado como RSA, permitiendo proteger la información sensible que queramos transportar. Como podemos incluir cualquier tipo de datos en el payload, debido a su flexibilidad, contamos con la facilidad de adaptar el JWT a cualesquiera parámetros para la autenticación que queramos implementar en nuestra aplicación.

Referencias.

--

--