Encabezados HTTP en la API del cliente Websockets
Parece que es fácil agregar encabezados HTTP personalizados a su cliente websocket con cualquier cliente de encabezado HTTP que admita esto, pero no encuentro cómo hacerlo con la WebSocket
API de la plataforma web.
¿Alguien tiene idea de cómo lograrlo?
var ws = new WebSocket("ws://example.com/service");
Específicamente, necesito poder enviar un encabezado de autorización HTTP.
Actualizado 2x
Respuesta corta: No, solo se pueden especificar la ruta y el campo de protocolo.
Respuesta más larga:
No existe ningún método en la API de JavaScript WebSockets para especificar encabezados adicionales para que los envíe el cliente/navegador. La ruta HTTP ("GET /xyz") y el encabezado del protocolo ("Sec-WebSocket-Protocol") se pueden especificar en el constructor WebSocket.
El encabezado Sec-WebSocket-Protocol (que a veces se extiende para usarse en la autenticación específica de websocket) se genera a partir del segundo argumento opcional del constructor WebSocket:
var ws = new WebSocket("ws://example.com/path", "protocol");
var ws = new WebSocket("ws://example.com/path", ["protocol1", "protocol2"]);
Lo anterior da como resultado los siguientes encabezados:
Sec-WebSocket-Protocol: protocol
y
Sec-WebSocket-Protocol: protocol1, protocol2
Un patrón común para lograr la autenticación/autorización de WebSocket es implementar un sistema de emisión de tickets donde la página que aloja al cliente WebSocket solicita un ticket del servidor y luego pasa este ticket durante la configuración de la conexión WebSocket, ya sea en la URL/cadena de consulta, en el campo de protocolo, o requerido como primer mensaje después de establecer la conexión. Luego, el servidor solo permite que la conexión continúe si el ticket es válido (existe, no se ha utilizado aún, la IP del cliente está codificada en las coincidencias del ticket, la marca de tiempo en el ticket es reciente, etc.). Aquí hay un resumen de la información de seguridad de WebSocket: https://devcenter.heroku.com/articles/websocket-security
La autenticación básica era anteriormente una opción, pero ha quedado obsoleta y los navegadores modernos no envían el encabezado incluso si está especificado.
Información de autenticación básica (en desuso, ya no funcional) :
NOTA: la siguiente información ya no es precisa en ningún navegador moderno.
El encabezado de Autorización se genera a partir del campo de nombre de usuario y contraseña (o simplemente nombre de usuario) del URI de WebSocket:
var ws = new WebSocket("ws://username:[email protected]")
Lo anterior da como resultado el siguiente encabezado con la cadena "nombre de usuario: contraseña" codificada en base64:
Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
Probé la autenticación básica en Chrome 55 y Firefox 50 y verifiqué que la información de autenticación básica se negocia con el servidor (es posible que esto no funcione en Safari).
Gracias a Dmitry Frank por la respuesta de autenticación básica.
Es más una solución alternativa, pero todos los navegadores modernos envían las cookies del dominio junto con la conexión, por lo que se utiliza:
var authToken = 'R3YKZFKBVi';
document.cookie = 'X-Authorization=' + authToken + '; path=/';
var ws = new WebSocket(
'wss://localhost:9000/wss/'
);
Termine con los encabezados de conexión de solicitud:
Cookie: X-Authorization=R3YKZFKBVi
Aquí hay un breve resumen de la situación para el viajero cansado que se topa con esto en 2024, y probablemente durante mucho tiempo después de eso.
Nada ha cambiado en los 12 (!) años transcurridos desde que se abrió esta pregunta. Todos los proveedores de navegadores abandonan la API JavaScript WebSocket (aunque las implementaciones ocasionalmente reciben actualizaciones ), y las nuevas especificaciones ( WebSocket Stream y WebTransport ) no están ni cerca de materializarse. Lo que todo esto significa es que los WebSockets todavía se utilizan ampliamente, no existe ningún reemplazo para la API rota a pesar de que hace 7 años se la llamó "heredada" , y los problemas descritos en la pregunta son tan molestos como siempre, si no más.
Las opciones para lidiar con la situación (spoiler, #5 o #6 es lo que quieres):
1. Implementar la autenticación externamente
Descrito en https://devcenter.heroku.com/articles/websocket-security . Se espera que el cliente realice una solicitud autenticada a un punto final dedicado que generará y mantendrá un token de corta duración que también se enviará al cliente. Luego, el cliente devuelve este token como parámetro de URL al abrir un WebSocket. El servidor puede validarlo y aceptar/rechazar la actualización del protocolo. Esto requiere que el servidor implemente un mecanismo de autenticación con estado completamente personalizado y específico para WebSockets, lo cual es un puente demasiado lejos en muchos escenarios.
2. Enviar información de autenticación a través de WebSocket
Abre un WebSocket sin autenticarse, luego envía su información de autenticación a través de WebSocket antes de hacer cualquier otra cosa. En teoría, esto suena lógico (y los proveedores de navegadores lo recomiendan ), pero se desmorona si se lo piensa brevemente. El servidor está diseñado para implementar un mecanismo de autenticación incómodo, con mucho estado y totalmente personalizado que no funciona bien con nada más, además de tener que mantener una conexión persistente con un cliente que se niega a autenticarse, dejando una puerta abierta para ataques de denegación de servicio o meterse en un asunto completamente nuevo al imponer tiempos de espera rigurosos para evitar comportamientos maliciosos.
3. Enviar información de autenticación (por ejemplo, un token de acceso) a través de un parámetro de URL
No es tan terrible como parece, siempre y cuando se aplique SSL ( wss://
no ws://
) porque las URL de WebSocket son especiales y no se guardan en el historial del navegador o similar. Además de eso, los tokens de acceso normalmente tienen una vida corta, lo que también mitiga el peligro. Pero. Es muy probable que el servidor registre la URL de todos modos en algún momento. Incluso si su aplicación de servidor no lo hace, el marco o el host (en la nube) probablemente lo harán. Además, si tiene que pasar tokens de identificación (como suele hacer Firebase), puede tropezar con varias limitaciones de longitud de URL a medida que los tokens de identificación se vuelven enormes.
4. Autenticación a través de una buena cookie antigua
No. Los WebSockets no están sujetos a una política del mismo origen (porque aparentemente todo lo relacionado con los WebSockets tiene que ser horrible) y permitir cookies lo dejaría completamente expuesto a ataques CSRF. Arreglar este problema utilizando tokens CSRF se describe, por ejemplo, aquí , pero es más difícil que adoptar cualquier otro enfoque de esta lista, por lo que simplemente ni siquiera vale la pena considerarlo.
5. Contrabandear tokens de acceso al interiorSec-WebSocket-Protocol
Dado que el único encabezado que un navegador le permitirá controlar es Sec-WebSocket-Protocol
, puede abusar de él para emular cualquier otro encabezado. Curiosamente (o más bien cómicamente), esto es lo que está haciendo Kubernetes . En resumen, agrega todo lo que necesita para la autenticación como un subprotocolo compatible adicional dentro Sec-WebSocket-Protocol
:
var ws = new WebSocket("ws://example.com/path", ["realProtocol", "yourAccessTokenOrSimilar"]);
Luego, en el servidor, agrega algún tipo de middleware que transforma la solicitud a su forma más sana antes de pasarla al sistema. Terrible, sí, pero hasta ahora la mejor solución. Sin tokens en la URL, sin autenticación personalizada salvo por el pequeño middleware, no se necesita estado adicional en el servidor.
6. Cambie a SSE (¿o RSocket?), si corresponde
En un buen número de casos, la ESS podría ser un sustituto decente. La EventSource
API del navegador está terriblemente rotaWebSocket
(no puede enviar nada más que solicitudes GET, tampoco puede enviar encabezados a pesar de ser HTTP normal), ¡pero! se puede reemplazar fácilmente porfetch
una API más sensata, para variar. Este enfoque funciona bien como alternativa a WebSocket, por ejemplo, en suscripciones GraphQL, o en cualquier lugar donde el dúplex completo no sea obligatorio. Y eso probablemente cubra la mayoría de los escenarios. En teoría, RSocket también podría ser una opción, pero al ver cómo se implementa a través de WebSockets en el navegador, no creo que realmente resuelva nada, pero no lo investigué lo suficientemente profundo como para decirlo con absoluta certeza.
No es posible enviar el encabezado de autorización.
Adjuntar un parámetro de consulta de token es una opción. Sin embargo, en algunas circunstancias, puede no ser conveniente enviar su token de inicio de sesión principal en texto sin formato como parámetro de consulta porque es más opaco que usar un encabezado y terminará registrándose quién sabe dónde. Si esto le plantea problemas de seguridad, una alternativa es utilizar un token JWT secundario solo para el socket web .
Cree un punto final REST para generar este JWT , al que, por supuesto, solo pueden acceder los usuarios autenticados con su token de inicio de sesión principal (transmitido a través del encabezado). El JWT del socket web se puede configurar de manera diferente a su token de inicio de sesión, por ejemplo, con un tiempo de espera más corto, por lo que es más seguro enviarlo como parámetro de consulta de su solicitud de actualización.
Cree un JwtAuthHandler separado para la misma ruta en la que registra el SockJS eventbusHandler . Asegúrese de que su controlador de autenticación esté registrado primero, para que pueda comparar el token del socket web con su base de datos (el JWT debe estar vinculado de alguna manera a su usuario en el backend).
El problema del encabezado de autorización HTTP se puede solucionar con lo siguiente:
var ws = new WebSocket("ws://username:[email protected]/service");
Luego, se establecerá un encabezado HTTP de autorización básica adecuado con el archivo username
y password
. Si necesita autorización básica, entonces ya está todo listo.
Yo quiero usarlo Bearer
sin embargo, y recurrí al siguiente truco: me conecto al servidor de la siguiente manera:
var ws = new WebSocket("ws://[email protected]/service");
Y cuando mi código en el lado del servidor recibe el encabezado de Autorización básica con un nombre de usuario no vacío y una contraseña vacía, interpreta el nombre de usuario como un token.