OAuth2 & Spring Framework
Recientemente tuvimos el desafío de implementar para un cliente un nuevo sistema de autorización y autenticación de aplicaciones, microservicios y usuarios. Lo primero que nos vino a la mente es OAuth2. Nunca lo habíamos implementado, pero sabíamos que es hoy el estándar usado por todas las grandes empresas para autenticar sus usuarios y “compartir” sus APIs con aplicaciones de terceros.
Ventajas
- Protocolo estándar, ampliamente usado y de seguridad probada.
- Gran variedad de librerías y frameworks que permiten integrarse a OAuth2, incluyendo Spring Framework.
- Gran variedad de “workflows” tanto para autenticar usuarios como para autorizar aplicaciones, y fáciles de implementar.
- Usándolo en conjunto con el estándar JWT, permite generar tokens stateless y firmados digitalmente, lo que permite no solo no tener que persistirlos para poder luego validarlos, sino también que sea la misma aplicación que recibe el tokens quien valide la autenticidad del mismo mediante la firma digital.
El primer paso fue leer acerca del protocolo, y aunque parezca avanzando, el mejor punto de partida es la misma especificación: RFC-6749, al leerla nos damos cuenta que es mucho más sencilla de lo que nos imaginamos.
La solución tenía que estar basada en Spring, y por suerte el framework tiene un módulo que implementar por completo el estándar: Spring Security OAuth.
Segundo paso: comenzar a programar! aquí lo más difícil es cómo empezar, Spring nos da un montón de posibilidades y muchos ejemplos, y teníamos que desarrollar un servidor OAuth2 que “autorize” con tokens a aplicaciones a acceder a otras aplicaciones/APIs, luego modificar una de las APIs para que acepte los tokens otorgados por el primero, y por último modificar las apps clientes para que soliciten los tokens. Demasiado para comenzar desde cero sin un único punto de referencia. Por suerte encontré un gran proyecto open source en Github que implementa todo esto y que fue nuestro punto de partida: OAuth2 with JWT.
Este proyecto de ejemplo consta de 3 apps, todos desarrollados con Spring-Boot:
- client: pequeña app web que intenta consumir un recurso protegido, por lo que primero debe solicitar un token. Para consumir ambos servicios (el servicio que otorga los tokens y el servicio que expone el recurso protegido) utiliza el cliente REST de Spring:
RestTemplate
. Lo bueno de esta librería, es que con sólo configurarle los datos del servicio OAuth2, automáticamente implementará todo el workflow de solicitar y refrezcar el token cuando sea necesario. - resource: App REST que expone un endpoint protegido, solo accesible mediante un token JWT otorgado por el servidor OAuth2.
- oauth: Servidor OAuth2, otorga tokens JWT firmados asimétricamente a usuarios que valida mediante una clase que contiene una pequeña base de datos en memoria y hardcodeados. Se puede reemplazar fácilmente con otra clase que implemente un datastore “real”, como una base de datos relacional, NoSQL, o un servicio REST externo.
Tokens JWT
JWT no es parte del estándar OAuth2, pero es cada vez más usado para implementar tokens OAuth2 debido a sus características y sencillez.
Un mensaje JWT es simplemente un JSON, con un contenido determinado por el emisor (en nuestro caso el servidor OAuth2) encodeado en Base64Url para que sea fácilmente transportable en URLs y headers, al cual se le concatena separados con un punto “.” como prefijo un header también en formato JSON y encodeado en Base64Url, y al final del mensaje también separados por un “.” se le concatena una firma digital del cuerpo anterior. El algoritmo usado para firmar el mensaje es especificado en el header antes mencionado, ya que se pueden usar algoritmos simétricos o asimétricos (recomendado).
Cuando JWT es usado por un servidor OAuth2, el mismo suele grabar en el JSON información acerca de la identidad del usuario y sus privilegios, lo que es llamado en el mundo “JWT” como “claims”. Puntualmente un token generado por un servidor OAuth2 de Spring puede tener la siguiente información:
{ "authorities": ["CLIENT_TEST"], "scope": ["rw"], "jti": "aebebea6-24cc-4477-82fb-688d6356b1b5", "client_id": "trusted", "exp": 1483209345}
Este token indica que el client_id usado (algo así como el id/usuario de aplicación) es “trusted”, que posee una authority (rol) llamada “CLIENT_TEST”, y el campo exp indica en formato “timestamp” en que fecha expira el token.
Este “claim” es encodeado en Base64Url, y como mencionamos antes se le concatena al principio un header que identifica al mensaje como un mensaje JWT y qué algoritmo de firmado usa (también en Base64Url):
Al final todo este mensaje encodeado se lo firma digitalmente, concatenando el resultado también al token separado con un “.”, quedando así nuestro token:
Tips
- Cuando al consumir el servidor OAuth desde una app no obtenemos una respuesta esperada, y no sabemos si el error está en la aplicación que consume o en la que expone el servicio, probar con otra herramienta para obtener los tokens, que nos permita ver claramente tanto el mensaje enviado como la respuesta junto con los headers HTTP. Mi consejo: curl, o HTTPie, en combinación con la opción
-v
que tienen ambos comandos, para ver no solo el body de respuesta del servidor, sino también los headers HTTP enviados y recibidos, que son parte importante de la comunicación con un servidor OAuth. - Una de las ventajas de usar tokens JWT otorgados centralizadamente pero de validación des-centralizada es que el token puede ser intercambiado entre las distintas apps, y la app que recibe el token puede interpretar la información del mismo, y confiar que ese token es de confianza y pertenece a quien dice pertenecer confirmando que la firma haya sido creada con la clave pública. Pero eso no significa que el usuario tendrá permisos ilimitados, en el token se puede asignar distintos permisos o roles (authorities). Usar nombre de roles cortos (para que el token no crezca mucho en tamaño), pero que identifiquen bien qué puede hacer no solo en una app, sino dentro de todo el stack de apps de nuestra organización. Ejemplo: si vamos a otorgar permisos de administrador a alguien en una app “backoffice”, pueden darle un rol
BACK_ADMIN
, el prefijo indica la app, mientras que el sufijo el rol, pero si luego ese token lo recibe otra API, puede que esta API confíe en los usuarios del backoffice, y quiera darle ciertos privilegios de acceso a los administradores de la misma, por eso es importante que el rol sea inequívoco.
Si necesitan implementar en una aplicación Node.js un cliente que autentique contra OAuth2, pueden usar una librería open source que desarrollamos para tal fin, basada en la librería request: reqclient. Al igual que la clase RestTemplate
de Spring, tiene una clase (sí, Javascript ahora soporta clases…) llamada RequestClient
que implementa automáticamente los workflows de OAuth2, y muchos otros agregados útiles.