Anuncios

Bienvenidos sean a este post, hoy hablaremos sobre JSON Web Token.

Anuncios

Este es un estandar abierto basado en JSON para crear tokens que afirman cierto numero de afirmaciones, este tipo de token se compone de tres secciones separadas por un punto, en el formato A.B.C donde:

  • A, es el algoritmo que se usa para calcular la firma
  • B, es la carga util (payload)
  • C, es la firma que se utiliza para verificar la validez del token
Anuncios
Anuncios

Todas estan codificadas con una codificacion segura Base64 para URL y de ahora en adelante nos referiremos a ella como base64url, siendo Base64 un esquema de codificacion de binario a texto muy popular que representa datos binarios en una cadena ASCII traduciendolos a una representacion radix-64, esta utiliza las letras A-Z, a-z y los numeros 0-9 mas los simbolos + y / dando un total de 64 simbolos en total, como lo indica su nombre, por ejemplo es utilizado para codificar imagenes adjuntas en un correo electronico, esto sucede tan inadvertidamente que el mundo no se entera de esto.

Anuncios
Nota: 
esto se realiza con base64url debido a que los caracteres + y / son usados en URL para los espacios y separadores respectivamente y aqui son reemplazados por - y _ respectivamente permitiendonos una perfecta codificacion.
Anuncios
Anuncios

Por lo tanto la manera que este tipo de tokens trabajan es ligeramente diferente a la que usabamos cuando trabajamos con hashes, de hecho la informacion que transporta el token es siempre visible por lo tanto solo se necesita decodificar los puntos A y B para obtener el algoritmo y la carga util, sin embargo la seguridad radica en el punto C que es el hash HMAC del token, y si se intenta modificar la parte B editando la carga util, volviendolo a codificar a Base64 y reemplazandolo en el token, la firma no coincidira mas y por lo tanto sera un token invalido, por ejemplo podemos crear una carga util que informa estar logueado como un admin, o algo por el estilo, y mientras el token sea valido podemos confiar que el usuario esta logueado como admin, antes de trabajar con nuestro ejemplo vamos a instalar el modulo:

Para instalar en Debian:
$ sudo apt-get install python3-jwt
Anuncios
Para instalar un Windows mediante pip:
pip install jwt
Anuncios

Con nuestro modulo instalado pasemos a trabajar con un ejemplo y para ello importaremos el paquete pertinente:

>>> import jwt
Anuncios

Con nuestro modulo importado pasemos a crear un diccionario:

>>> datos = {'cargautil' : 'datos', 'id' : 100123}
Anuncios

En este caso creamos unos datos donde pasaremos un id y la carga util o payload que mencionamos antes, nuestro siguiente paso sera codificarlo y para ello usaremos la siguiente linea:

token = jwt.encode(datos, 'clave-secreta')
Anuncios

En este caso creamos un objeto donde guardaremos la codificacion hecha con encode donde le pasamos el diccionario creado anteriormente y una «clave secreta», con esto realizado pasemos a crear un nuevo objeto:

>>> datos_sal = jwt.decode(token, 'clave-secreta')
Anuncios

Este objeto lo usaremos para almacenar la decodificacion hecha anteriormente, para esto usaremos a decode pero pasaremos el objeto creado anteriormente y la misma clave, pasemos a ver el valor almacenado en token:

>>> print(token)
b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjYXJnYXV0aWwiOiJkYXRvcyIsImlkIjoxMDAxMjN9.usjo24ymyE77ygafReitZCx6uKaPlhFZiQNGoXpI39U'
>>>
Anuncios

Como vemos se codifico perfectamente, veamos el valor de datos_sal:

>>> print(datos_sal)
{'cargautil': 'datos', 'id': 100123}
>>>
Anuncios

Como podemos ver se decodifico perfectamente y obtuvimos el resultado original, en este ejemplo no informamos ningun algoritmo por lo tanto la funcion toma el hash predeterminado que es HS256, vamos a volver a codificar los valores de datos pero esta vez con otro algoritmo:

>>> token512 = jwt.encode(datos, 'clave-secreta', algorithm='HS512')
Anuncios

Este es similar al anterior pero le pasamos el algoritmo, vamos a decodificarlo:

>>> datos_sal = jwt.decode(token512, 'clave-secreta', algorithm='HS512')
Anuncios

Al igual que en el caso anterior informamos el algoritmo porque de lo contrario no podra decodificarlo, el resto es exactamente lo mismo, si verificamos su salida debemos obtener el mismo resultado que antes, veamos una ultima forma de decodificarlo:

>>> datos_sal = jwt.decode(token512, verify=False, algorithm='HS512')
Anuncios

Esta opcion no es segura pero nos permite decodificar el token sin necesidad de verificarlo contra la contraseña, prueben y deben obtener el mismo resultado, pasemos al siguiente tema.

Anuncios

Reclamaciones registradas

Veamos el siguiente listado:

  • iss, el emisor del token
  • sub, la información del sujeto sobre la parte sobre la que este token lleva información
  • aud, la audiencia para el token
  • exp, el tiempo de expiracion despues del cual el token es considerado invalido
  • nbf, el no anterior (tiempo) o el tiempo antes del cual el token se considera no válido todavía
  • iat, la hora en que el token fue emitido
  • jdt, el id del token
Anuncios

Y a su vez puede ser categorizado de esta forma:

  • Privado, son aquellos definidos por los usuarios (consumidores y productores) de JWT, es decir que se trata de declaraciones ad hoc que se utilizan para un caso particular, por lo tanto se debe usar con cuidado para evitar colisiones
  • Publico, son aquellas que estan registradas en el registro de reclamaciones IANA JSON Web Token, un registro donde los usuarios pueden registrar sus reclamaciones y asi evitar colisiones.
Anuncios

Con todo esto comentado pasemos a ver algunas de estas reclamaciones con algunos ejemplos.

Anuncios

Reclamaciones relacionadas al tiempo

Para este ejemplo vamos a crear un archivo llamado reclamo_tiempo.py y le agregaremos el siguiente codigo:

reclamo_tiempo.py

from datetime import datetime, timedelta
from time import sleep
import jwt

iat = datetime.utcnow()
nbf = iat + timedelta(seconds=1)
exp = iat + timedelta(seconds=3)
datos = {'cargautil' : 'datos', 'nbf' : nbf, 'exp' : exp, 'iat' : iat}

def decodificar(token, secreto):
	print(datetime.utcnow().time().isoformat())
	try:
		print(jwt.decode(token, secreto))
	except (
		jwt.ImmatureSignatureError, jwt.ExpiredSignatureError
	) as err:
		print(err)
		print(type(err))

secreto = 'clave-secreta'
token = jwt.encode(datos, secreto)

decodificar(token, secreto)
sleep(2)
decodificar(token, secreto)
sleep(2)
decodificar(token, secreto)
Anuncios
Anuncios

Primero importaremos todos los paquetes necesarios, el primer paquete nos permite tener herramientas para manejar el tiempo, la siguiente nos proporciona una herramienta para generar una pausa y la ultima es la de jwt, despues tenemos un bloque con distintas variables, en la primera guardamos el tiempo actual, en la segunda agregamos una duracion de 1 segundo al tiempo almacenado en la anterior, en la tercera volvemos a repetir lo anterior pero con una duracion de tres segundos, si observan se llaman igual que los reclamos antes vistos, luego en datos tenemos un campo que representa a la carga util (payload) y despues pasamos las reclamaciones antes creadas, despues tenemos una funcion para decodificar.

Anuncios

Esta funcion recibe dos datos, uno para el token y otro para la clave secreta, dentro del bloque lo primero que haremos sera mostrar el tiempo actual en formato iso, despues tenemos un bloque try/except donde trabajaremos sobre la instruccion encargada de mostrar el valor decodificado del token, y en except buscaremos dos excepciones:

  • la primera es para cuando no esta preparado
  • la otra para cuando expiro
Anuncios

Todo esto lo almancenamos con el nombre de err, y en este bloque mostramos la excepcion ocurrida y el tipo de excepcion, con esto concluimos nuestra funcion.

Anuncios

Nuestro siguiente paso sera crear la variable que usaremos como clave secreta, y luego crearemos el token por medio de encode, por ultimo ejecutaremos la funcion con el token y la clave, haremos una pausa de 2 segundos volvemos a ejecutar la funcion, esperamos otros 2 segundos y volveremos a ejecutar la funcion, con esto terminamos con el codigo veamos que sucede:

tinchicus@dbn001vrt:~/lenguajes/python$ python3 reclamo_tiempo.py 
03:56:16.047645
The token is not yet valid (nbf)
<class 'jwt.exceptions.ImmatureSignatureError'>
03:56:18.050542
{'cargautil': 'datos', 'nbf': 1618977377, 'exp': 1618977379, 'iat': 1618977376}
03:56:20.053779
Signature has expired
<class 'jwt.exceptions.ExpiredSignatureError'>
tinchicus@dbn001vrt:~/lenguajes/python$
Anuncios

Vean que la primera llamada devuelve el error debido a que establecimos una demora de 1 segundo y como todavia no se valido al token este nos devuelve un error, despues de la pausa si lo ejecutamos este nos devuelve los valores almacenados en datos, por ultimo la ultima llamada nos devuelve otro error debido a establecimos a exp para que expire a los 3 segundos de creado, es decir con esto vimos como las reclamaciones afectan a nuestro codigo y solo nos permiten acceder a este cuando es valido, pasemos al siguiente tema.

Anuncios

Reclamaciones relacionadas con al autenticacion

Para este caso vamos a crear un ejemplo donde trabajaremos con iss y aud, comencemos creando un archivo llamado reclamo_aut.py y le agregaremos el siguiente codigo:

reclamo_aut.py

import jwt

datos = {'cargautil':'datos', 'iss':'tinchicus', 'aud':'tinchicus.com'}
secreto = 'clave-secreta'
token = jwt.encode(datos, secreto)

def decodificar(token, secreto, issuer=None, audience=None):
	try:
		print(jwt.decode(
			token, secreto, issuer=issuer, audience=audience))
	except (
		jwt.InvalidIssuerError, jwt.InvalidAudienceError
	) as err:
		print(err)
		print(type(err))

decodificar(token, secreto)
decodificar(token, secreto, audience='tinchicus.com')
decodificar(token, secreto, issuer='tinchicus')
decodificar(token, secreto, issuer='nope', audience='tinchicus.com')
decodificar(token, secreto, issuer='tinchicus', audience='nope')
decodificar(token, secreto, issuer='tinchicus', audience='tinchicus.com')
Anuncios
Anuncios

Primero importamos a jwt, luego crearemos un diccionario con la carga util y las dos reclamaciones que pondremos en practica, luego la clave que usaremos y por ultimo generamos el token, con el token generado definiremos la funcion decodificar, este es muy similar al anterior porque en el bloque try decodificaremos el token informando las reclamaciones iss (issuer) y aud (audience), pero en el except chequeamos por dos excepciones para estos reclamos, y como en el ejemplo anterior mostramos el error y el tipo, despues haremos todas las posibles llamadas que podemos realizar a decodificar, veamos como es su salida para hablar de cada una:

tinchicus@dbn001vrt:~/lenguajes/python$ python3 reclamo_aut.py 
Invalid audience
<class 'jwt.exceptions.InvalidAudienceError'>
{'cargautil': 'datos', 'iss': 'tinchicus', 'aud': 'tinchicus.com'}
Invalid audience
<class 'jwt.exceptions.InvalidAudienceError'>
Invalid issuer
<class 'jwt.exceptions.InvalidIssuerError'>
Invalid audience
<class 'jwt.exceptions.InvalidAudienceError'>
{'cargautil': 'datos', 'iss': 'tinchicus', 'aud': 'tinchicus.com'}
tinchicus@dbn001vrt:~/lenguajes/python$
Anuncios

Observen que la primer llamada nos devuelve un error de audiencia invalida, la segunda informando una audiencia correcta pero sin el emisor (issuer) nos devolvera la salida, la tercera sin la audiencia nos devolvera un error, los siguientes dos casos donde informamos a issuer o a audience con un valor incorrecto en ambos casos nos devolvera un error y en la ultima llamada nos devolvio nuevamente los datos, con todo esto explicado podemos pasar al ultimo tema.

Anuncios

Usando algoritmos asimetricos (clave publica)

Algunas veces utilizar una contraseña no es la mejor opcion en seguridad pero si disponemos de un sistema operativo basado en Unix, p.e. Linux, podemos hacer esto mismo con claves RSA, pero que es una clave RSA?

Anuncios

La criptografia de clave publica, o criptografia asimetrica, es cualquier sistema criptografico que usa pares de claves:

  • claves publicas, estas pueden difundirse ampliamente
  • claves privadas, solo conocidas por el propietario
Anuncios

Vamos a crear dos pares de claves, uno sera sin contraseña y el otro con contraseña, para comenzar vamos a crear un nuevo directorio y lo llamaremos rsa, dentro de este directorio ejecutaremos el siguiente comando:

$ ssh-keygen -t rsa -m PEM
Anuncios

Este pasara el algoritmo a usar (rsa) y el forma que debemos usar (PEM), luego nos devolvera lo siguiente:

Generating public/private rsa key pair.
Enter file in which to save the key (/home/tinchicus/.ssh/id_rsa): clavepwd
Anuncios

Aqui podemos especificar una ubicacion diferente o simplemente utilizar la sugerida presionando Enter, para nuestro ejemplo pondremos clavepwd (como se ve) porque sera la clave que tendra contraseña, presionamos Enter a lo cual aparecera lo siguiente:

Enter passphrase (empty for no passphrase):
Anuncios

Aqui nos pedira que ingresemos una contraseña para la clave, en este caso les recomiendo la que use ‘Clave123’ sin las comillas, cuando presionen Enter aparecera lo siguiente:

Enter same passphrase again:
Anuncios

Aqui debemos repetir la misma contraseña y presionan Enter para ingresarla, si todo sale bien nos aparecera lo siguiente:

Your identification has been saved in clavepwd.
Your public key has been saved in clavepwd.pub.
The key fingerprint is:
SHA256:wEM//rLQUXBwC8mw4JFsN/kNgsdn2+3oPkL2Zw7jFWg tinchicus@dbn001vrt
The key's randomart image is:
+---[RSA 2048]----+
|   .o+o++oo      |
|   .==Bo*= .     |
|   ..o=*o=o.     |
|       +ooo..    |
|        S Eo.    |
|       .o+. ..   |
|      .ooo+ .    |
|       ..+++o    |
|        .oo=.    |
+----[SHA256]-----+
Anuncios

Esto nos indica que se creo correctamente las claves privadas y publicas, volvemos a repetir lo mismo pero esta vez a la clave la llamaremos clave y al momento de presionar establecer una contraseña la dejamos en blanco simplemente presionando Enter las dos veces, si todo salio bien y probamos de listarlo veremos lo siguiente:

tinchicus@dbn001vrt:~/lenguajes/python/rsa$ ls -l
total 16
-rw------- 1 tinchicus tinchicus 1831 abr 21 16:23 clave
-rw-r--r-- 1 tinchicus tinchicus  401 abr 21 16:23 clave.pub
-rw------- 1 tinchicus tinchicus 1876 abr 21 16:22 clavepwd
-rw-r--r-- 1 tinchicus tinchicus  401 abr 21 16:22 clavepwd.pub
tinchicus@dbn001vrt:~/lenguajes/python/rsa$
Anuncios

Como podemos observar se crearon las dos claves, una privada y otra publica, pasemos a nuestro ejemplo y para ello bajaremos un directorio y crearemos un nuevo archivo al cual llamaremos token_rsa.py y le agregaremos el siguiente codigo:

token_rsa.py

import jwt
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization

datos = {'payload' : 'datos'}

def codificar(datos, archivo_priv, pwd_priv=None, algorithm='RS256'):
	with open(archivo_priv, 'rb') as clave:
		clave_privada = serialization.load_pem_private_key(
			clave.read(),
			password=pwd_priv,
			backend=default_backend()
		)
	return jwt.encode(datos, clave_privada, algorithm=algorithm)


def decodificar(datos, archivo_pub, algorithm='RS256'):
	with open(archivo_pub, 'rb') as clave:
		clave_publica = clave.read()
	return jwt.decode(datos, clave_publica, algorithm=algorithm)


token=codificar(datos, 'rsa/clave')
datos_sal=decodificar(token, 'rsa/clave.pub')
print(datos_sal)

token=codificar(datos, 'rsa/clavepwd', pwd_priv=b'Clave123')
datos_sal=decodificar(token, 'rsa/clavepwd.pub')
print(datos_sal)
Anuncios

Primero vamos a importar todas nuestras necesidades, el primero es el modulo jwt, seguido de una clase llamada serialization y una funcion llamada default_backendla que nos seran muy utiles para la primera funcion, luego creamos un pequeño diccionario como hasta ahora, pasemos ver la primera funcion:

def codificar(datos, archivo_priv, pwd_priv=None, algorithm='RS256'):
	with open(archivo_priv, 'rb') as clave:
		clave_privada = serialization.load_pem_private_key(
			clave.read(),
			password=pwd_priv,
			backend=default_backend()
		)
	return jwt.encode(datos, clave_privada, algorithm=algorithm)
Anuncios
Anuncios

Esta sera la funcion para codificar, recibira los datos a codificar, el archivo que usaremos como clave privada, luego la password del archivo pero con un valor predeterminado de None para los casos que no tengan, por ultimo el algoritmo que debemos usar, despues abriremos el archivo y lo leeremos en bytes y lo pasaremos al objeto llamado clave, despues crearemos un nuevo objeto llamado clave_privada, en este usaremos un metodo de serialization llamada load_pem_private_key, esta se encargara de cargar la clave privada en formato pem (el cual fue establecido al momento de crearla), a este le pasaremos tres atributos:

  • El primer atributo es el dato almacenado en la clave privada
  • El segundo atributo es la contraseña del archivo en caso de existir
  • El tercer atributo es llamado backend porque sera utilizado solo por este metodo y para este atributo pasamos la funcion importada al comienzo
Anuncios

Con los tres atributos informados usamos un return para devolver la codificacion creada por medio de los datos pasados, la clave privada creada anteriormente y por ultimo el algoritmo que debemos usar, pasemos a la siguiente funcion:

def decodificar(datos, archivo_pub, algorithm='RS256'):
	with open(archivo_pub, 'rb') as clave:
		clave_publica = clave.read()
	return jwt.decode(datos, clave_publica, algorithm=algorithm)
Anuncios

Esta funcion sera la encargada de decodificar, pasaremos los datos, el archivo de clave publica y el algoritmo que debemos usar, lo siguiente sera utilizar un open para abrir el archivo informado lo pasamos a un objeto llamado clave y dentro crearemos un objeto que llamaremos clave_publica y almacenaremos todo lo leido del archivo, para finalmente devolver la decodificacion de los datos informados, gracias a la clave publica y el algoritmo, con todo esto explicado podemos pasar a probar, para ello usamos dos casos veamos el primero:

token=codificar(datos, 'rsa/clave')
datos_sal=decodificar(token, 'rsa/clave.pub')
print(datos_sal)
Anuncios

Este bloque es utilizado para probar la clave privada pero sin contraseña, primero crearemos el objeto llamado token y usaremos a codificar donde pasaremos a datos y el lugar y nombre del archivo sin contraseña, con nuestro token creado vamos a crear otro objeto llamado datos_sal y usaremos a decodificar, para este caso pasaremos el token antes generado pero ahora enviaremos el archivo publico (.pub) por ultimo mostramos lo almacenado en datos_sal, veamos el siguiente bloque:

token=codificar(datos, 'rsa/clavepwd', pwd_priv=b'Clave123')
datos_sal=decodificar(token, 'rsa/clavepwd.pub')
print(datos_sal)
Anuncios

Este es muy similar al anterior pero aqui usaremos a la otra clave privada, y despues pasamos la clave de la misma, la siguiente es igual pero usaremos la clave publica (sin contraseña) y por ultimo mostramos los datos, veamos que sucede en su ejecucion:

tinchicus@dbn001vrt:~/lenguajes/python$ python3 token_rsa.py 
{'payload': 'datos'}
{'payload': 'datos'}
tinchicus@dbn001vrt:~/lenguajes/python$
Anuncios

Como podemos ver funciono perfectamente tanto sin contraseña como con contraseña, esto es solo un ejemplo practico para ver como una codificacion por medio de una clave asincronica puede establecer una mayor seguridad que una por medio de clave secreta o contraseña, con esto hemos cubierto algunas de las formas mas usuales para trabajar con jwt.

Anuncios

En resumen, hoy hemos visto que es el JSON Web Token, mas conocido como jwt, hemos visto su forma mas basica de trabajo, despues hemos visto algunas de las reclamaciones que tiene registradas, tambien hemos como pueden ser accedidas, hemos visto algunos ejemplos donde aplicamos estas reclamaciones y por ultimo hemos visto como trabajar con una clave generada por medio de ssh, espero les haya sido util sigueme en tumblr, Twitter o Facebook para recibir una notificacion cada vez que subo un nuevo post en este blog, nos vemos en el proximo post.

Anuncios
pp258

Donación

Es para mantenimento del sitio, gracias!

$1.50