Securizar un API REST utilizando JSON Web Tokens

29
26606

Índice de contenidos


1. Introducción

En este tutorial veremos cómo securizar un API REST empleando JSON Web Tokens (JWT). Para este tutorial utilizaremos un API muy simple donde activaremos Spring Security con la configuración adecuada e implementaremos las clases necesarias para el uso de JWT.

En anteriores tutoriales vimos con securizar un API REST utilizando Node.js y JWT, en esta ocasion utilizaremos Spring Boot ya que nos permite desarrollar rápidamente API REST con el mínimo código y por tanto con el menor número de errores 😉

Disponéis de un tutorial de Natalia Roales en el portal sobre proyectos con Spring Boot y os dejamos el código fuente de este tutorial en github para vuestra consulta.


2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil Mac Book Pro 15″ (2,5 Ghz Intel Core i7, 16 GB DDR3)
  • Sistema Operativo: Mac OS Sierra 10.12.6
  • Entorno de desarrollo: Spring Tool Suite 3.9.0


3. JSON Web Token

JSON Web Token (JWT) es un estándar abierto (RFC 7519) que define un modo compacto y autónomo para transmitir de forma segura la información entre las partes como un objeto JSON. Esta información puede ser verificada y es confiable porque está firmada digitalmente. Los JWT se pueden firmar usando un secreto (con el algoritmo HMAC) o utilizando un par de claves públicas / privadas usando RSA.


3.1. Ventajas de los tokens frente a las cookies

Quizás la mayor ventaja de los tokens sobre las cookies es el hecho de que no tenga estado (stateless). El backend no necesita mantener un registro de los tokens. Cada token es compacto y auto contenido. Contiene todos los datos necesarios para comprobar su validez, así como la información del usuario para las diferentes peticiones.

El único trabajo del servidor consiste en firmar tokens al iniciar la sesión y verificar que los tokens intercambiados sean válidos. Esta característica permite la escalabilidad inmediata ya que las peticiones no dependen unas de otras. De esta forma se pueden tramitar en diferentes servidores de forma autónoma.

Las cookies funcionan bien con dominios y subdominios únicos, pero cuando se trata de administrar cookies en diferentes dominios, puede volverse complejo. El enfoque basado en tokens con Cross Origin Resource Sharing (CORS) habilitado hace trivial exponer las API a diferentes servicios y dominios.

Con un enfoque basado en cookies, simplemente se almacena el ID de sesión en una cookie. JWT por otro lado permiten almacenar cualquier tipo de metadatos, siempre y cuando sea un JSON válido.

Si os preguntáis por el rendimiento, cuando se utiliza la autenticación basada en cookies, el backend tiene que hacer una búsqueda habitualmente una base de datos para recuperar la información del usuario, esto seguramente supera el tiempo que pueda tomar la decodificación de un token. Además, puesto que se pueden almacenar datos adicionales dentro del JWT, por ejemplo, permisos de usuario, puede ahorrarse llamadas adicionales para la búsqueda de esta información.

Recordad que el token habitualmente va firmado pero no va cifrado. En la web de jwt.io disponéis de un depurador que permite consultar y comprobar la validez del mismo.

Después de tanta teoría vamos a lo interesante.


4. Estructura del proyecto

Nuestro proyecto a nivel de Maven está configurado para ser un proyecto Spring Boot de tipo web, que utiliza Spring Security, JPA y HSQLDB (podéis consultar el pom.xml). Veamos las partes más importantes del ejemplo, consta de un controlador para acceso al API REST, el acceso a la capa de datos por JPA y los beans de dominio, muy simple. Para esta demostración utilizaremos la base de datos en memoria HSQLDB (HyperSQL Database).

A nivel de controlador se disponen de métodos para crear un usuario, consultar todos o consultar uno concreto. Para evitar almacenar las password en plano, aplicamos una función de Hash basada en el cifrado Blowfish (BCrypt). Recordad que el uso de MD5 para almacenar las password no se recomienda, utilizad algoritmos más modernos.

UsuarioController.java


5. Spring Security

Gracias a Spring Security podemos incorporar mecanismos potentes para proteger nuestras aplicaciones utilizando una cantidad mínima de código.

En nuestro caso indicamos a Spring Security que proteja todas las URLs excepto la URL de login, así mismo, declaramos las implementaciones que utilizaremos para realizar la autenticación y autorización.

WebSecurity.java

Podemos observar que se ajusta la configuración para CORS y se desactiva el filtro de Cross-site request forgery (CSRF). Esto nos permite habilitar el API para cualquier dominio, esta es una de las grandes ventajas del uso de JWT.


6. Implementación

En el siguiente diagrama podéis observar el flujo habitual de una aplicación securizada. Si lo comparamos al flujo seguido en la autenticación vía cookies, es muy similar.

Por fin llegamos a la lógica de negocio a utilizar para autenticar y autorizar nuestras peticiones. Para simplificar este tutorial, se verificará únicamente que exista el usuario y password en nuestra base de datos. Se podría incorporar un modelo más complejo incorporando permisos y roles pero se aleja del objetivo de este tutorial. Si os interesa estos temas, podéis consultar su manejo en la documentación de Spring Security.


6.1. Autenticación

Haciendo uso de las clases proporcionadas por Spring Security, extendemos su comportamiento para reflejar nuestras necesidades. Se verifica que las credencias proporcionadas son válidas y se genera el JWT.

JWTAuthenticationFilter.java

No hay obligación de devolver el token en la cabecera ni con una clave concreta pero se recomienda seguir los estándares utilizados en la actualidad (RFC 2616, RFC 6750). Lo habitual es devolverlo en la cabecera HTTP utilizando la clave “Authorization” e indicando que el valor es un token “Bearer “ + token

Este token lo deberá conservar vuestro cliente web en su localstorage y remitirlo en las peticiones posteriores que se hagan al API.


6.2. Autorización

La clase responsable de la autorización verifica la cabecera en busca de un token, se verifica el token y se extrae la información del mismo para establecer la identidad del usuario dentro del contexto de seguridad de la aplicación. No se requieren accesos adicionales a BD ya que al estar firmado digitalmente si hay alguna alteración en el token se corrompe.

JWTAuthenticationFilter.java


7. Probando…probando

Para las pruebas seguiremos el flujo que vimos previamente, primero, se invoca al login para recuperar el token y posteriormente invocaremos las llamadas al API utilizando el token obtenido.

Una vez arrancamos la aplicación y lanzamos los comandos obtendremos algo parecido a la imagen adjunta. Si intentamos invocar alguna URL sin el token obtendremos un código de error HTTP 403.


8. Conclusiones

La securización de API es un tema muy extenso y hemos visto una pequeña parte en este tutorial. Como habréis podido observar Spring nos facilita la incorporación de JWT a nuestras APIs gracias a su “magia”. En el caso que no dispongáis de esta posibilidad, os animo a consultar los frameworks y librerías existentes ya que cada vez está más extendido el uso de los tokens para diferentes lenguajes.

Espero que os haya servido


9. Referencias

29 Comentarios

  1. Buenas,

    La librería utilizada jjwt y Spring security puede proporcionarte toda la funcionalidad que necesitas. Aunque el tema puede dar para otro tutorial, a grandes rasgos, tienes que recuperar de la BD los roles del usuario e incorporarlo al contexto de seguridad de Spring Security. En la clase UsuarioDetailsServiceImpl al devolver los datos del usuario podemos incorporar una lista con las autorizaciones del usuario (GrantedAuthority).

    Una vez se disponga de las autorizaciones podemos incluir en el token que se entrega al usuario, la lista de roles para que vuestro frontal se comporte conforme a estas autorizaciones.Como mencioné anteriormente podemos personalizar nuestros tokens e incluir la información que se desee, por ejemplo en un campo «roles» e incluir una lista con los nombres de los roles asociados del usuario, consulta los métodos de la clase Jwts para incluir más campos.

    Por ultimo en el lado de vuestra API REST, al recibir las peticiones securizadas de los usuarios, durante el parseo del token asociado se puede recuperar del token la lista de autorizaciones. Esto evita consultar nuevamente los permisos del usuario en la BD.

    Espero haber respondido a tu duda,

    Un saludo

  2. Hola, me parecio interesante todo pero mi duda esta si me la puedes aclarar por favor, mencionaste que debemos guardar el jwt en el localstorage, pero como puedo guardar el jwt en esa memoria desde la clase java, ya que solo puedo acceder a localStorage desde javascript.
    Y luego, al realizar alguna peticion tendra que ser todas por ajax para enviar con el jwt? porque, como digo en el parrafo anterior, una clase java no tiene acceso al localStorage, solo el javascript.

    • Buenas,

      El API que se ha desarrollado en el ejemplo se centra únicamente en el lado del servidor. Podéis desarrollar el frontal web o vuestro cliente web en cualquier lenguaje, únicamente deben remitir correctamente el token generado al API. Hago referencia al localstorage por dar ideas y ser un mecanismo muy utilizado para almacenar este tipo de tokens pero dependiendo del cliente que se utilice se debe utilizar el mecanismo adecuado.

      Un saludo

  3. Hola, me gusta mucho el tema y estoy desarrollando un proyecto usando json web tokens. Me preguntaba si es posible, desde la parte del cliente, editar solo la parte del payload del token (sin saber la clave) y remitir el token con esa parte cambiada al API y que sea valido.
    En mi caso me gustaría almacenar el tipo de rol del usuario en el token, para así cuando el usuario cliente me lo devuelva puedo comprobar su rol y darle acceso a determinadas acciones o no. Pero claro, en caso que se puede editar el payload desde el cliente, sin modificar la firma, no sería seguro porque cualquiera puede editar su rol dentro del token.
    Un saludo

    • Buenas,

      No me queda muy claro lo que intentas montar pero suena extraño que desde el lado cliente se indique el rol que se tiene en la aplicación.

      Piensa en el token como en un ticket de aparcamiento, por mucho que lo modifique a mano, la hora de entrada la sabe el sistema, lo mismo pasa con los roles. Si el token no está alterado o expirado, en el lado servidor no hace falta volver a consultar los roles nuevamente.
      La gracia de indicarte el rol en el token es que el lado cliente puede personalizar la interfaz en función de los datos del token.

      Espero que te sirva de ayuda.

      Un saludo.

  4. Buenas, tengo una duda, si interceptaran la petición GET o POST, podrían coger el token del campo Authorization y realizar todas las consultas posibles con ese token no es asi? hasta que dejara de ser valido si tiene un tiempo de expiracion, hay alguna forma de solucionarlo si fuera cierto?

    Un saludo

    • Buenas,

      Podrían utilizarlo mientras el token esté vigente, no deja de ser tráfico http «en plano».
      Podrías cifrar el JWT pero no suele ser lo normal, es preferible cifrar todo el tráfico del servidor utilizando SSL para evitar que puedan el leer todo el contenido (incluida las cabeceras).

      Espero que te sirva de ayuda.

      Un saludo

  5. Trato de implementar el registro de usuarios agregando otra ruta que no pase por el filtro (igual que /login) pero igual me redirecciona a /login.
    1. ¿Qué podría estar haciendo mal?
    2. ¿Cómo puedo hacer que las respuestas no sean html ?

    Saludos.

    • Buenas,

      Acerca de tu primera pregunta, hay que ajustar el filtro de Spring Security (revisa la clase WebSecurity.java para incluir las rutas que no se validan). Como las combinaciones son infinitas te recomiendo que revises la documentación de Spring Security.

      Sobre la segunda, no me queda muy claro. El tutorial explica el uso de JWT en un API REST (Json), si quieres incluir otros contenidos revisa la documentación de Spring MVC donde te detallan las diferentes posibilidades.

      Espero que te sirva.

      Un saludo

  6. Hola, exelente tutorial; solo tengo una duda, cómo mapeas la ruta htt://localhost:8080/login.

    He revisado todo el código y no encuentro dicha configuración, disculpa si la pregunta es trivial pero soy nuevo en Spring.

    Saludos.

  7. Hola excelente articulo, me quedan algunas dudas y no se si se pueda hacer, entiendo que cuando realizas este paso source.registerCorsConfiguration(«/**», new CorsConfiguration().applyPermitDefaultValues()); solo se van aceptar peticiones POST, GET y HEAD, pero que pasa si quiero aceptar PUT y DELETE?,

  8. Hola, tengo un error de 403 Forbbiden al enviar las credenciales por PostMan a la ruta localhost:8080/login, implementé el código tal y aparece en el tutorial, que podría ser?

    • La URL de login es quien te proporciona el token con el que puedes realizar el resto de llamadas. Si las credenciales fueran incorrectas devolvería un 401 (no autorizado) y no un 403.
      Suena a que la configuración de tu aplicación es incorrecta.

      Descargate el código fuente de github y compáralo con tu implementación.

      Espero que te sirva de ayuda

    • Buenas,

      No hace falta, los campos que interesan (usuario, password) los recupera la clase JWTAuthenticationFilter. En la configuración de la seguridad (WebSecurity) se indica que la URL correspondiente al login no incluye token porque obviamente a través del login se te genera el token.

      Espero que te sirva,
      Un saludo

  9. Hola,
    de antemano gracias por el tutorial. Me gustaría aplicarle el tema de roles. En el método loadUserByUsername ya estoy cargando los roles que tengo definidos y estoy usando @PreAuthorize en los endpoints para restringir el acceso. Sin embargo con la sola autenticación se accede a los endpoints, como puedo restringir la autorización?

  10. Hola, qué tal? en la clase JWTAuthenticationFilter, tengo un error de compilación, me extraña porque copie el mismo código del repositorio de github.

    Usuario credenciales = new ObjectMapper().readValue(request.getInputStream(), Usuario.class);

    Esta línea me da el error. Dice lo siguiente el compilador: The type com.fasterxml.jackson.core.JsonParser cannot be resolved. It is indirectly referenced from required .class files.

    Gracias por responder. Saludos.

    • Buenas,

      Según indicas a tu proyecto le falta alguna librería. Revisa la configuración de Maven, es posible que tu repositorio esté corrupto (directorio .m2) y no pueda acceder a alguna librería.

      Espero que te sirva de ayuda,

      Un saludo

  11. Que tal, tengo una duda, en que momento declaras el metodo /login en el controller? Ya que yo trato de implementar un login que ya voy y valido en mi base de datos si exixte mi usuario pero no se en que momento se inicializan las clases para poder generar el token. Saludos desde CDMX.

    • Buenas,

      En esta demostración no hace falta declararlo en el controlador, se define en el fichero WebSecurity que la URL terminada en «/login» no requiera credenciales (token) puesto que en esta petición se suministra las credenciales iniciales con las que se genera el token (observa el diagrama).
      Los filtros que se indican en el tutorial se encargan de la autenticación y de la autorización (es configurable). Se basa en Spring Security, dependiendo de las necesidades de tu negocio la implementación de la seguridad varía.

      La documentación de Spring Security ofrece mucha información sobre las diferentes posibilidades.

      Espero que te sirva,
      Un saludo

    • Buenas,

      Spring Boot detecta que se trata de una base de datos en memoria y por convención al arrancar la aplicación lanza el script import.sql para inicializar la BD. Es parte de la magia de Spring ya que te evita tener que configurar lo mismo en todos tus proyectos.

      En la documentación de Spring tienes más detalles.

      Espero que te sirva,
      Un saludo

    • Buenas,

      Para hacer la demostración del uso de tokens he incluido un usuario cualquiera (en este caso admin). En este ejemplo NO se han incluido roles, permisos o similar, es un nombre como otro cualquiera.

      Espero que te sirva,
      Un saludo

Dejar respuesta

Please enter your comment!
Please enter your name here