Autenticación basada en tokens API REST

122

Estoy desarrollando una API REST que requiere autenticación. Debido a que la autenticación en sí ocurre a través de un servicio web externo a través de HTTP, razoné que distribuiríamos tokens para evitar llamar repetidamente al servicio de autenticación. Lo que me lleva a mi primera pregunta:

¿Es esto realmente mejor que simplemente exigir a los clientes que utilicen la autenticación básica HTTP en cada solicitud y el almacenamiento en caché de las llamadas al servicio del servidor de autenticación?

La solución de autenticación básica tiene la ventaja de no requerir un viaje de ida y vuelta completo al servidor antes de que puedan comenzar las solicitudes de contenido. Los tokens pueden ser potencialmente más flexibles en su alcance (es decir, solo otorgar derechos a recursos o acciones particulares), pero eso parece más apropiado para el contexto de OAuth que mi caso de uso más simple.

Actualmente los tokens se adquieren así:

curl -X POST localhost/token --data "api_key=81169d80...
                                     &verifier=2f5ae51a...
                                     &timestamp=1234567
                                     &user=foo
                                     &pass=bar"

El api_key, timestampy verifierson requeridos por todas las solicitudes. El "verificador" es devuelto por:

sha1(timestamp + api_key + shared_secret)

Mi intención es permitir solo llamadas de partes conocidas y evitar que las llamadas se reutilicen textualmente.

¿Es esto lo suficientemente bueno? Underkill? ¿Exceso?

Con un token en la mano, los clientes pueden adquirir recursos:

curl localhost/posts?api_key=81169d80...
                    &verifier=81169d80...
                    &token=9fUyas64...
                    &timestamp=1234567

Para la llamada más simple posible, esto parece horriblemente detallado. Teniendo en cuenta que la aplicación shared_secretterminará incrustada (como mínimo) en una aplicación de iOS, de la cual supongo que se puede extraer, ¿esto incluso ofrece algo más allá de una falsa sensación de seguridad?

cantlin
fuente
2
En lugar de usar sha1 (marca de tiempo + api_key + shard_secret) debe usar hmac (shared_secret, timpestamp + api_key) para un mejor hashing de seguridad en.wikipedia.org/wiki/Hash-based_message_authentication_code
Miguel A. Carrasco
@ MiguelA.Carrasco Y parece ser el consenso en 2017 de que bCrypt es la nueva herramienta de hashing.
Shawn

Respuestas:

94

Permítanme separar todo y resolver cada problema de forma aislada:

Autenticación

Para la autenticación, baseauth tiene la ventaja de que es una solución madura a nivel de protocolo. Esto significa que muchos de los problemas de "podría surgir más tarde" ya están resueltos para usted. Por ejemplo, con BaseAuth, los agentes de usuario saben que la contraseña es una contraseña, por lo que no la almacenan en caché.

Carga del servidor de autenticación

Si distribuye un token al usuario en lugar de almacenar en caché la autenticación en su servidor, aún está haciendo lo mismo: almacenar en caché la información de autenticación. La única diferencia es que usted está transfiriendo la responsabilidad del almacenamiento en caché al usuario. Esto parece una mano de obra innecesaria para el usuario sin ganancias, por lo que recomiendo manejar esto de manera transparente en su servidor como lo sugirió.

Seguridad de transmisión

Si puede usar una conexión SSL, eso es todo, la conexión es segura *. Para evitar la ejecución múltiple accidental, puede filtrar varias URL o solicitar a los usuarios que incluyan un componente aleatorio ("nonce") en la URL.

url = username:[email protected]/api/call/nonce

Si eso no es posible, y la información transmitida no es secreta, recomiendo asegurar la solicitud con un hash, como sugirió en el enfoque de token. Dado que el hash proporciona la seguridad, puede indicar a sus usuarios que proporcionen el hash como contraseña de baseauth. Para mejorar la robustez, recomiendo usar una cadena aleatoria en lugar de la marca de tiempo como "nonce" para evitar ataques de repetición (se pueden hacer dos solicitudes legítimas durante el mismo segundo). En lugar de proporcionar campos separados de "secreto compartido" y "clave de API", simplemente puede usar la clave de API como secreto compartido, y luego usar una sal que no cambie para evitar ataques de la mesa arcoiris. El campo de nombre de usuario también parece un buen lugar para poner el nonce, ya que es parte de la autenticación. Así que ahora tienes una llamada limpia como esta:

nonce = generate_secure_password(length: 16);
one_time_key = nonce + '-' + sha1(nonce+salt+shared_key);
url = username:[email protected]/api/call

Es cierto que esto es un poco laborioso. Esto se debe a que no está utilizando una solución de nivel de protocolo (como SSL). Por lo tanto, podría ser una buena idea proporcionar algún tipo de SDK a los usuarios para que al menos no tengan que pasar por ellos mismos. Si necesita hacerlo de esta manera, considero que el nivel de seguridad apropiado (just-right-kill).

Almacenamiento secreto seguro

Depende de a quién intentes frustrar. Si está evitando que las personas con acceso al teléfono del usuario utilicen su servicio REST a nombre del usuario, sería una buena idea encontrar algún tipo de API de llavero en el sistema operativo de destino y que el SDK (o el implementador) almacene el Clave allí. Si eso no es posible, al menos puede hacer que sea un poco más difícil obtener el secreto cifrándolo y almacenando los datos cifrados y la clave de cifrado en lugares separados.

Si está tratando de evitar que otros proveedores de software obtengan su clave API para evitar el desarrollo de clientes alternativos, solo el enfoque de cifrar y almacenar por separado casi funciona. Esta es una criptografía de caja blanca, y hasta la fecha, nadie ha encontrado una solución verdaderamente segura para los problemas de esta clase. Lo menos que puede hacer es emitir una sola clave para cada usuario para que pueda prohibir las claves abusadas.

(*) EDITAR: las conexiones SSL ya no deberían considerarse seguras sin tomar medidas adicionales para verificarlas .

cmc
fuente
Gracias cmc, todos los puntos buenos y buena comida para pensar. Terminé adoptando un enfoque de token / HMAC similar al que discutió anteriormente, más bien como el mecanismo de autenticación S3 REST API .
cantlin el
Si almacena en caché el token en el servidor, ¿no es esencialmente lo mismo que el id de sesión anterior? La identificación de la sesión es de corta duración y también se adjunta al almacenamiento en caché rápido (si lo implementa) para evitar golpear su base de datos en cada solicitud. El verdadero diseño RESTful y sin estado no debería tener sesiones, pero si está utilizando un token como ID y luego sigue presionando la base de datos, ¿no sería mejor usar la ID de sesión? Alternativamente, puede buscar tokens web JSON que contengan información encriptada o firmada para datos de sesión completa para un verdadero diseño sin estado.
JustAMartin
16

Una API RESTful pura debe usar las características estándar del protocolo subyacente:

  1. Para HTTP, la API RESTful debe cumplir con los encabezados estándar HTTP existentes. Agregar un nuevo encabezado HTTP viola los principios REST. No vuelva a inventar la rueda, use todas las características estándar en los estándares HTTP / 1.1, incluidos los códigos de respuesta de estado, encabezados, etc. Los servicios web RESTFul deberían aprovechar y confiar en los estándares HTTP.

  2. Los servicios RESTful DEBEN ESTAR SIN ESTADO. Cualquier truco, como la autenticación basada en token que intenta recordar el estado de las solicitudes REST anteriores en el servidor, viola los principios REST. De nuevo, esto es IMPRESCINDIBLE; es decir, si su servidor web guarda cualquier información relacionada con el contexto de solicitud / respuesta en el servidor en un intento de establecer cualquier tipo de sesión en el servidor, entonces su servicio web NO tiene estado. Y si NO es apátrida NO es RESTFul.

En pocas palabras: para fines de autenticación / autorización, debe usar el encabezado de autorización estándar HTTP. Es decir, debe agregar el encabezado de autenticación / autorización HTTP en cada solicitud posterior que deba autenticarse. La API REST debe seguir los estándares del Esquema de autenticación HTTP. Los detalles de cómo se debe formatear este encabezado se definen en los estándares RFC 2616 HTTP 1.1 - sección 14.8 Autorización de RFC 2616, y en la RFC 2617 Autenticación HTTP: Autenticación de acceso básica y resumida .

Desarrollé un servicio RESTful para la aplicación Cisco Prime Performance Manager. Busque en Google el documento de REST API que escribí para esa aplicación para obtener más detalles sobre el cumplimiento de RESTFul API aquí . En esa implementación, he optado por utilizar el esquema de autorización HTTP "Básico". - consulte la versión 1.5 o superior de ese documento REST API y busque autorización en el documento.

Rubens Gomes
fuente
8
"Agregar un nuevo encabezado HTTP viola los principios REST" ¿Cómo es eso? Y si lo hace, puede ser tan amable de explicar cuál es exactamente la diferencia (con respecto a los principios) entre una contraseña que caduca después de un cierto período y un token que caduca después de un cierto período.
un mejor oliver
66
Nombre de usuario + contraseña es un token (!) Que se intercambia entre un cliente y un servidor en cada solicitud. Ese token se mantiene en el servidor y tiene un tiempo de vida. Si la contraseña caduca, tengo que adquirir una nueva. Parece asociar "token" con "sesión de servidor", pero esa es una conclusión no válida. Incluso es irrelevante porque sería un detalle de implementación. Su clasificación de tokens que no sean nombre de usuario / contraseña como con estado es puramente artificial, en mi humilde opinión.
un mejor oliver
1
Creo que debería motivar por qué hacer una implementación con RESTful sobre la autenticación básica, que es parte de la pregunta original. Tal vez también podría vincular a algunos buenos ejemplos con código incluido. Como principiante en este tema, la teoría parece bastante clara con muchos buenos recursos, pero el método de implementación no lo es y los ejemplos son complicados. Me resulta frustrante que parezca que se necesita una codificación personalizada para implementar de manera oportuna algo que se ha hecho miles de veces.
JPK
13
-1 "Cualquier truco, como la autenticación basada en token que intenta recordar el estado de las solicitudes REST anteriores en el servidor viola los principios REST". La autenticación basada en token no tiene nada que ver con el estado de las solicitudes REST anteriores y no viola la apatridia de REST .
Kerem Baydogan el
1
Entonces, de acuerdo con esto, ¿los tokens web JSON son una violación de REST porque pueden almacenar el estado del usuario (reclamos)? De todos modos, prefiero violar REST y usar una buena ID de sesión antigua como "token", pero la autenticación inicial se realiza con nombre de usuario + contraseña, firmada o encriptada usando secreto compartido y marca de tiempo de muy corta duración (por lo que falla si alguien intenta reproducirla) ese). En una aplicación "Enterprise-ish" es difícil desechar los beneficios de la sesión (evitando golpear la base de datos para obtener algunos datos necesarios en casi todas las solicitudes), por lo que a veces tenemos que sacrificar la verdadera apatridia.
JustAMartin
2

En la web, un protocolo con estado se basa en tener un token temporal que se intercambia entre un navegador y un servidor (a través del encabezado de cookies o la reescritura de URI) en cada solicitud. Ese token generalmente se crea en el extremo del servidor, y es una pieza de datos opacos que tiene un cierto tiempo de vida, y tiene el único propósito de identificar un agente de usuario web específico. Es decir, el token es temporal y se convierte en un ESTADO que el servidor web debe mantener en nombre de un agente de usuario cliente durante la duración de esa conversación. Por lo tanto, la comunicación que usa un token de esta manera es ESTATUA. Y si la conversación entre el cliente y el servidor es ESTATUA, no es RESTANTE.

El nombre de usuario / contraseña (enviado en el encabezado de Autorización) generalmente se conserva en la base de datos con la intención de identificar a un usuario. A veces, el usuario puede significar otra aplicación; sin embargo, el nombre de usuario / contraseña NUNCA tiene la intención de identificar un agente de usuario de cliente web específico. La conversación entre un agente web y un servidor basada en el uso del nombre de usuario / contraseña en el encabezado de la Autorización (después de la Autorización Básica de HTTP) NO TIENE NINGÚN TIPO porque el front-end del servidor web no está creando ni manteniendo ninguna información ESTATALen nombre de un agente de usuario de cliente web específico. Y según mi comprensión de REST, el protocolo establece claramente que la conversación entre los clientes y el servidor debe ser ESTÁTICA. Por lo tanto, si queremos tener un verdadero servicio RESTful, debemos usar el nombre de usuario / contraseña (consulte RFC mencionado en mi publicación anterior) en el encabezado de autorización para cada llamada, NO un token de tipo sensorial (por ejemplo, tokens de sesión creados en servidores web , Tokens OAuth creados en servidores de autorización, etc.).

Entiendo que varios proveedores llamados REST están usando tokens como OAuth1 u OAuth2 accept-tokens para ser pasados ​​como "Autorización: Portador" en los encabezados HTTP. Sin embargo, me parece que el uso de esos tokens para servicios RESTful violaría el verdadero significado SIN ESTADO que REST abarca; porque esos tokens son datos temporales creados / mantenidos en el lado del servidor para identificar un agente de usuario de cliente web específico durante la duración válida de una conversación de ese cliente / servidor web. Por lo tanto, cualquier servicio que esté utilizando esos tokens OAuth1 / 2 no debe llamarse REST si queremos mantener el VERDADERO significado de un protocolo STATELESS.

Rubens

Rubens Gomes
fuente