En cualquier API tener claro el mecanismo de autenticación de usuarios es algo clave.
Tenemos dos casos de uso de autenticación:
- Por un lado, que un usuario inicie sesión con nosotros por primera vez en esta sesión. Aquí, el usuario tendrá que autenticarse de algún modo operable por humanos (usuario y contraseña; OAuth; LDAP; puntos biométricos u otras estrategias). Llamemos a estas credenciales, proporcionadas por el usuario, credenciales primarias.
- Por otro lado, una vez autenticado por primera vez, una forma eficiente de mantener una comunicación abierta, de modo que podamos verificar que sigue siendo el mismo usuario el que se comunica con nosotros. Para esto, nos tendrá que enviar algún tipo de información que llamaremos credenciales secundarias.
Es importante salvaguardar especialmente las credenciales primarias, ya que es más problemático obligar a resetear estas que unas credenciales secundarias por-sesión.
Por lo tanto, es interesante distinguirlas y proveer un mecanismo de autenticación basada en tokens, para evitar almacenar en las plataformas cliente las credenciales primarias de autenticación.
Knox
Existe una aplicación para Django REST Framework denominada Knox que soluciona este problema.
Knox permite crear n tokens por usuario (cada uno identificando una sesión), que pueden expirar automáticamente o perdurar hasta que se cierre sesión manualmente (al borrarlos de la base de datos).
Una vez obtenido (haciendo POST a un determinado lugar del API), puede incluirse en el resto de peticiones como una cabecera:
Authentication: Token 123456789abcdef
Los tokens de Knox son criptográficamente seguros: el token sale de un PRNG, y nunca es guardado en base de datos. En su lugar, se almacena una versión hasheada y salteada. De este modo, aunque un atacante pudiese obtener un dump de la base de datos no podría encontrar credenciales válidas con las que suplantar a ningún usuario.
Esto es cojonudo y fantástico. Sin embargo, al no haber en base de datos ningún hint que relacione el token que estamos recibiendo en la cabecera de autenticación, esto significa que en cada petición hay que recuperar todos los tokens de la base de datos, saltear y hashear el token recibido con cada uno de los salts guardados y comparar.
Esta operación es lenta, y puede dar lugar, entre otros, a ataques de timing, por el cual un atacante MITM pueda obtener información sobre si un usuario se ha autenticado satisfactoriamente o no, dependiendo del tiempo que tarde el servidor en contestar a las peticiones.
Sin embargo, almacenar en la base de datos información sobre el token no parece razonable ya que disminuiría las garantías de seguridad que proporciona esta infraestructura.
JSON Web Tokens (JWT)
JSON Web Tokens es un estándar de Internet (RFC 7519) que habla de autenticación stateless: sin guardar información en el servidor.
WTF?
Sí: si podemos enviarle a nuestro cliente un texto cifrado con información autocontenida de su propio login, con MAC (Message Authentication Code) con un secreto que solo conoce el servidor, entonces el servidor puede simplemente descifrar el mensaje y confiar en que, si coinciden los checksums, el usuario es quien dice ser en el interior del mensaje cifrado (ya que el mensaje lo cifró el mismo servidor, que lo autenticó en primer lugar).
Explicado sencillo consiste en lo siguiente:
- Alice quiere ser autenticada por Bob, con lo que le envia sus credenciales primarias.
- Bob, para no pedírselas cada vez, le entrega un maletín cerrado con llave y a prueba de modificaciones (con un dispositivo que invalida el maletín si se modifica).
Alice cada vez que quiere autenticarse le entrega el maletín a Bob, que lo abre con su llave y observa que en su interior hay un documento que dice: Esto representa a Alice si son menos de las 12. Firmado, Bob.
Bob le devuelve el maletín (u opcionalmente se lo cambia por otro con contenido de una fecha posterior). Lo cierra con llave de nuevo y se lo devuelve a Alice.
Problema: El servidor no tiene control del número de sesiones que tenga un usuario abiertas, y un usuario no puede "Cerrar todas las sesiones" fácilmente, ya que los JWT seguirán siendo válidos hasta que expiren por si mismos (del mismo modo, resulta un vector de attaque importante el tener un JWT con una validez muy prolongada en el tiempo).
Solución: JWT + JID
JWT permite, dentro del payload que representa a la entidad a autenticar, introducir un JSON Web Token ID (JID). Este id debería tener una probabilidad negligible de colisiones, y puede utilizarse para evitar ataques de replay (según el RFC).
En nuestra arquitectura podríamos utilizar JSON Web Sockets para almacenar en la propia credencial de autenticación los siguientes dos datos:
De este modo, ganamos lo mejor de ambos lados (comprometiendo stateless a cambio de poder cerrar sesiones remotas):
- Detección del usuario con el que se pretende autenticar en el proceso de reconocimiento del JWT.
- Si el JWT no es válido, no es necesario tocar la base de datos.
- Fácil obtención del knox token: no es necesario recorrer todos los tokens válidos: en su lugar, podemos filtar por las sesiones abiertas por el usuario del JWT: reducimos potencialmente órdenes de magnitud el espacio de búsqueda, sin comprometer la seguridad y por le camino además mitigando los ataques de timing.
Resumen
Utilizar la infraestructura de creación de tokens criptográficamente seguros de Knox, y middleware que reemplace al de JWT y que incluya un JID con el que comprobar el token recibido mediante Knox.
Serán necesarias:
- Vistas API:
- auth/jwt/get_token
- auth/jwt/logout
- auth/jwt/logout_other
- auth/jwt/logout_all
- Middleware
- Modelos
- Ninguno. Es suficiente el de Knox.
Opcionalmente, puede ser interesante tener un registro de logins/logouts realizados a través de este API, y en ese caso sería prudente tener un modelo al respecto, con elementos tales como User-Agent o dirección IP (de cara a detección de botnets u otros vectores de ataque en el futuro).
Todo esto debería de ser una aplicación Django más y podría incluso residir en un repositorio independiente.
Me puedo encargar yo de ello si tengo tiempo.
Coste para el usuario final
El usuario podrá, obtener un token al autenticarse con sus credenciales que podrá utilizar a continuación en una cabecera Authentication: Bearer <XXX>
.
Beneficio para el usuario
El usuario podrá cerrar sesión de cada dispositivo de forma independiente, y podrá estar tranquilo de que sus credenciales no pueden ser robadas externamente.
Además, podrá saber cuántas sesiones tiene abiertas (cuáles son móviles o navegadores), y la última actividad en cada una de ellas (implementando la parte opcional).