En este artículo presento de una manera simple sin necesidad de conocimientos previos los conceptos principales detrás de JWT. ¿Qué implica su implementación? ¿Qué resuelve?
Partes que intervienen al usar JWT
Si ya te estás aburriendo y quieres ir directo al código lo puedes ver en https://github.com/jguastav/java-jwt-spring-sample/tree/master/jwt-demo
Objetivo
Utilizar un servicio disponible para ciertos usuarios sin proveerle las credenciales del usuario que está accediendo. Esta situación se puede dar por diversas razones. Un primer escenario puede darse cuando el usuario no quiere exponer sus credenciales ante el servicio o recurso que está solicitando. Otra razón puede ser que aún pudiendo exponer sus credenciales, sería más costoso en algún sentido hacerlo. En este segundo escenario, aún en la misma entidad pudiera existir un servicio que valida y confirma la identidad del usuario mediante sus credenciales, y una vez que esto sucede, el usuario ya podría utilizar todos los otros servicios sin necesidad de autenticarse en cada uno.
En otras palabras, significa demostrar que eres quien eres donde hay un tercero que confirma esta informacion. En algunos casos, como el segundo escenario citado, puede ser otra entidad la que identifique al usuario y en otros casos simplemente un servicio especializado de la misma entidad.
Una solución, definida mediante JWT es:
El Usuario U quiere acceder a un recurso para el cual tiene permisos en el servidor S.
En otras palabras, el servidor S tiene el recurso R disponible para el usuario U.
Pero el usuarios U no quiere definir un usuario y contraseña para identificarse con S.
Una razón es que pueden existir quasi infinitos seridores como S (llamemoslos S1, S2, S3, S4,…. Sn) y el usuario U no quiere definir un usuario / contraseña para cada uno… dado que sería muy engorroso de mantener si usara pares de usuario / contraseña distintos para cada uno. Y además sería muy “inseguro” usar el mismo usuario / contraseña … o la misma contraseña para todos. Si algún administrador de S3 no guarda las contraseñas de forma segura, alguien que trabaje en el entorno de S3 , pudiera entrar con las credenciales de U en S1 y acceder a sus datos.
El punto es que el usuario U quiere acceder al recurso R en el servidor S sin proveerle las credenciales. Y el recurso R no debe estar accesible para todo el público sino para aquellos que se identifiquen y tengan los permisos adecuados.
En el otro escenario el servidor S provee los servicios S-1, S-2, S-3,… S-n que requieren que el usuario esté identificado. Y existe un servicio S-I que identifica al usuario. El objetivo es que luego que el usuario U presentó sus credenciales al servicio S-I y fue identificado, pueda usar los servicios S-1, S-2… S-n. sin proveer las credenciales cada vez que requiera usar cada servicio.
JWT define una forma de lograrlo.
Una forma es involucrando un nuevo actor en esta relación, y este sería el servidor I (En el caso del escenario 1 sería el servicio S-I)
El servidor I es alguien en quien el usuario U y el servidor S “confían”. Entonces la idea es que el usuario U se identifique ante I con su usuario / contraseña, y que luego el usuario U al acceder a S, le presente algo por lo que S pueda confiar en U… y ese algo es algo provisto por el servidor I (que no es el username / contraseña de U)
Llegando hasta acá podemos enumerar los elementos que intervienen en la solución
Servidor S: Un servidor que contiene el recurso protegido
Recurso R: El recurso protegido
Usuario U: El usuario que quiere obtener el recurso en S sin identificarse (darle su usuario / password) ante S
Cliente C: Cuando el usuario “solicita” a S utiliza una aplicación, o parte de una aplicación, o un formulario en una aplicación. A este formulario o aplicación, lo llamamos Cliente C
Servidor I: El servidor I es el servidor / servicio ante el cual el Usuario U se identifica, ante quien el usuario U no tiene inconveniente en presentar sus credenciales (que podrían ser su Usuario y Contraseña o cualquier otro medio de identificación)
JWT : JSON Web Token: Standard
¿Cómo funciona JWT?
- El usuario U mediante el cliente C se identifica ante el servidor I (de autoenticación o Identificación) (por ejemplo con Usuario / Contraseña)
- El servidor (o servicio) I al dar por válida la identidad del usuario U genera un token T (una pieza de información) de acuerdo al standard JWT
- En cada petición subsiguiente que hace el cliente C hacia el servidor S envía el token T demostrando que tiene en su poder algo que fue generado para el usuario U por el servidor (o servicio) I
- El servidor S desencriptando el token T puede comprobar que la solicitud que está haciendo el cliente C tiene un elemento que realmente es del Usuario U
De esto surgen algunas preguntas
¿Cómo sabe el servidor S que el token T garantiza que el servidor I identificó al usuario U?
¿Cómo se evita que otro cliente U otro usuario utilice el token T para hacerle creer al servidor S o a otro servidor que proviene del usuario U?
La respuesta a estas dos preguntas se puede deducir a la respuesta a otra pregunta… que es ¿Qué información contiene el token?
El token contiene 3 partes llamadas
Header
Payload
Signature
Tanto el header como el payload se presentan encriptados de una forma que es muy fácil desencriptar.
Encriptar quiere decir cambiarle la forma o convertirlo en algo equivalente, con el objetivo de que no sea fácilmente legible, pero que se puede “desencriptar” o volver a su forma original.
El algoritmo para encriptar el header y el payload es base64 , una forma de encriptación y desencriptación que se puede encontrar en la web y con procesos online, o que hay variadas libraries para distintos lenguajes que lo hacen.
Un ejemplo de un site es https://www.base64decode.org/ Donde permite convertir un string a base64 o decodificar un valor base64 a su valor original
En cuanto al tercer valor, la signature, o firma es un dato encriptado que sólo puede ser producido por el Servidor (o servicio) I y nadie más. No sólo eso, sino que cualquiera puede comprobar que esa signature fue generada por el servidor I.
Qué datos hay en la signature? En la signature están el mismo header + el payload separados por un punto. Pero no en su formato original, sino en realidad es el header encodeado usando base64 + un punto + el payload encodeado usando base 64. (pareciera ser igual a las primeras 2 partes). No … porque en este caso, esa union de header encodeado + punto + payload encodeado … está “firmado” por el servidor I.
¿Qué quiere decir firmado? Quiere decir modificado, convertido o encriptado de tal forma que cualquiera que conozca al servidor I puede tener la certeza de que ese dato fue generado por el servidor I.
Entonces el token contiente
base64(header).base64(payload).signature(signedByServerI,base64(header).base64(payload))
Llamemos a estas partes
T = TH.TP.TS
Donde
- TH es base64(header)
- TP es base64(payload)
- TS es signature(signedByServerI,base64(header).base64(payload))
Con esto el servidor S puede determinar que esto fue generado por el servidor I
¿de qué forma?
El servidor S por conocer a I tiene una clave asociada a I con la que puede desencriptar TS
Si el servidor S al desencriptar TS con la clave de I determina que el dato desencriptado de TS es igual a TH.TP entonces el servidor S sabe que el token T fue generado por el servidor I.
Entonces tenemos que el servidor S tiene al menos un mecanismo por el cual puede saber si un token T fue generado por el servidor I o no.
En realidad hay más de un esquema de «firma» o identificar que el servidor o servicio I firmó la parte final del JWT.
1.- Tanto I como S usan la misma clave. Hay algoritmos donde la clave de encriptación es la misma que la clave que se usa para desencriptar. Entonces si los dos conocen la misma clave y esa clave es secreta, S puede confiar razonablemente que el token JWT fue generado por I. Esta es la opción comunmente usada en entorno de microservicios, donde la misma entidad maneja un servicio que identifica (S-I) y luego todos los demas servicios comprueban que el token que reciben se puede desencriptar con la misma clave.
2.- Existen algoritmos de encriptación y desencriptación donde la clave para encriptar es distinta que la que se usa para desencriptar. Y en tales algoritmos es muy común que sea practicamente imposible deducir una clave a partir de la otra. Entonces en esos casos, la autoridad que maneja el servidor I mantiene en secreto la clave para encriptar, y permite que todos los servidores que quieran recibir los tokens que genera conozcan la clave que desencripta. De esta forma llaman a la clave que desencripta «pública» y a la que encripta «privada». Esto es lo que normalmente se llama firma. Dado que todos los que pueden desencriptar el token con la clave pública provista, saben que el token fue generado con la clave privada que solo la autoridad que maneja el servidor I conoce.
Dado que el JWT se puede encriptar con diversos algoritmos de encriptación, el que desencripta el JWT debe saber qué algoritmo fue usado para encriptar. Esta es una de las informaciones que contiene el header en el JWT. Entonces el servidor (o servicio) S conociendo el header (donde se encuentra el algoritmo con que fue encriptado) y teniendo la clave para desencriptar puede leer y entender el contenido completo del JWT. y confirmar que el JWT fue generado por la autoridad que maneja el servidor (o servicio) I.
Cómo se identifica al usuario mediante el token JWT
Pero seguimos sin saber cómo hace el servidor S para saber que el token T identifica al usuario U
La respuesta está en el payload
Entre otros datos, el payload contiene al menos dos datos muy importantes.
1.- La identificación del usuario (podría ser un username del usuario)
2.- Un timestamp, o el tiempo en el cual fue generado el token
La forma es una expresión JSON que puede ser algo parecido a
{ "loggedInAs" : "jhonpepe123user", "iat" : 1422779638, "exp" : 1422779999 }
loggedIn: identifica al usuario
Iat: Issued at time identifica el momento en el que se generó el token
exp: Expiration date time
Entonces
El servidor S al hacer decode base 64 del TP (o el payload encodeado) puede obtener el id del usuario, por lo que puede saber ese usuario quien dice que es
Al desencriptar la firma y verificar que el valor del payload desencriptado es igual al payload de TP puede confirmar que el servidor I dijo que el usuario que aparece en TP es quien dice ser
Y el iat o issued time le indica al servidor S que ese token es válido sólo a partir del momento especificado en iat.
El exp indica una fecha y hora de vencimiento, tomando como invalido el token despues de ese momento.
Entonces en el payload existe suficiente informacion para determinar que segun el servidor I el usuario U es quien dice ser. Y el que tenga una fecha de generación y por lo tanto poder establecer una fecha y hora de vencimiento hace que ese token sea válido sólo por un tiempo suficientemente prudencial como para que no sea averiguado o copiado o generado. Tal vez se puede establecer un mecanismo para que el cliente C cada cierta cantidad de tiempo solicite un nuevo token al servidor I para continuar con la comunicación entre el servidor C y el servidor S
JWT Spring Java Código Fuente
El código fuente está disponible en
https://github.com/jguastav/java-jwt-spring-sample/blob/master/jwt-demo
Y su explicación en :
https://github.com/jguastav/java-jwt-spring-sample/blob/master/jwt-demo/README.md
La aplicación identifica a un usuario en 2 escenarios usando JWT: 1.- Escenario usando SECRET_KEY para encriptar (firmar) y desencriptar el token. Este es generalmente el escenario común para una arquitectura de servicios web donde el proveedor de los servicios es el mismo que proporciona el servicio de identificación. 2.- Escenario usando PRIVATE_KEY para cifrar (firmar) y PUBLIC_KEY para descifrar el token. Este es generalmente el escenario común cuando el emisor del servicio de identificación no es el mismo que los servicios y recursos que se obtienen posteriormente.
La SECRET_KEY del escenario 1 y PRIVATE_KEY y PUBLIC_KEY para el escenario 2 está disponible en la clase SamplePrivatePublicKeys.java Estas claves deben cambiarse en un entorno de producción en su propio escenario. La SECRET_KEY se puede generar manualmente. Cada vez que se ejecuta la aplicación se genera un nuevo par o PRIVATE_KEY y PUBLIC_KEY en los registros de la aplicación, en caso de que desee un nuevo par de claves públicas y privadas. Pero no es necesario que los uses. Es solo un servicio para mostrar cómo se generan. Las claves públicas y privadas que se muestran en el registro están codificadas en base64.