Encabezados HTTP en la API del cliente Websockets

201

Parece que es fácil agregar encabezados HTTP personalizados a su cliente websocket con cualquier cliente de encabezado HTTP que admita esto, pero no puedo encontrar cómo hacerlo con la API JSON.

Sin embargo, parece que debería haber soporte para estos encabezados en la especificación .

¿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.

Julien Genestoux
fuente
14
Creo que una buena solución es permitir que WebSocket se conecte sin autorización, pero luego bloquear y esperar en el servidor para recibir la autorización de webSocket que transmitirá información de autorización en su evento de apertura.
Motes
La sugerencia de @Motes parece ser la mejor opción. Fue muy fácil hacer una llamada de autorización desde onOpen que le permite aceptar / rechazar el socket en función de la respuesta de autorización. Originalmente intenté enviar el token de autenticación en el encabezado Sec-WebSocket-Protocol pero eso se siente como un truco.
BatteryAcid
@Motes Hola, ¿podrías explicar la parte de "bloquear y esperar en el servidor"? ¿Te refieres a algo como no procesar ningún mensaje hasta que haya un mensaje de "autenticación"?
Himal
@Himal, sí, el diseño del servidor no debe enviar datos ni aceptar ningún otro dato que no sea la autorización al comienzo de la conexión.
Motes
@Motes Gracias por la respuesta. Estaba un poco confundido por la parte de bloqueo, porque tengo entendido que no puede bloquear la connectsolicitud inicial . Estoy usando canales Django en el back-end y lo he diseñado para aceptar la conexión en connectcaso de evento. luego establece un indicador "is_auth" en el receiveevento (si ve un mensaje de autenticación válido). Si el indicador is_auth no está configurado y no es un mensaje de autenticación, cierra la conexión.
Himal

Respuestas:

212

Actualizado 2x

Respuesta corta: No, solo se puede especificar la ruta y el campo de protocolo.

Respuesta más larga:

No hay ningún método en la API de JavaScript WebSockets para especificar encabezados adicionales para que el cliente / navegador envíe. 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 para el constructor de 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 tickets donde la página que aloja al cliente de WebSocket solicita un ticket del servidor y luego pasa este ticket durante la configuración de la conexión de WebSocket, ya sea en la cadena URL / consulta, en el campo de protocolo, o requerido como el primer mensaje después de establecer la conexión. El servidor solo permite que la conexión continúe si el ticket es válido (existe, no se ha utilizado ya, la IP del cliente está codificada en coincidencias de tickets, 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 esto ha quedado en desuso y los navegadores modernos no envían el encabezado incluso si se especifica.

Información de autenticación básica (en desuso) :

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" base64 codificada:

Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=

He probado 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 (esto puede no funcionar en Safari).

Gracias a Dmitry Frank's por la respuesta básica de autenticación.

canaca
fuente
38
Me he encontrado con el mismo problema. Lástima que estos estándares estén tan pobremente integrados. Es de esperar que miren la API XHR para encontrar los requisitos (ya que WebSockets y XHR están relacionados) para la API WebSockets, pero parece que solo están desarrollando la API en una isla por sí misma.
Eleotlecram
44
@eleotlecram, únete al grupo de trabajo HyBi y proponlo. El grupo está abierto al público y hay trabajo en curso para las versiones posteriores del protocolo.
kanaka
55
@ Charlie: si controlas completamente el servidor, esa es una opción. El enfoque más común es generar un ticket / token desde su servidor HTTP normal y luego hacer que el cliente envíe el ticket / token (ya sea como una cadena de consulta en la ruta de websocket o como el primer mensaje de websocket). El servidor websocket luego valida que el ticket / token es válido (no ha expirado, no se ha utilizado, proviene de la misma IP que cuando se creó, etc.). Además, creo que la mayoría de los clientes de websockets admiten autenticación básica (aunque puede que no sea suficiente para usted). Más información: devcenter.heroku.com/articles/websocket-security
kanaka
3
Supongo que es por diseño. Tengo la impresión de que la implementación toma prestada intencionalmente de HTTP, pero los mantiene separados tanto como sea posible por diseño. El texto en la especificación continúa: "Sin embargo, el diseño no limita WebSocket a HTTP, y las implementaciones futuras podrían usar un protocolo de enlace más simple sobre un puerto dedicado sin reinventar todo el protocolo. Este último punto es importante porque los patrones de tráfico de la mensajería interactiva sí lo hacen. no coincide con el tráfico HTTP estándar y puede inducir cargas inusuales en algunos componentes ".
3
Lamentablemente, esto no parece funcionar en Edge. Gracias, MS: /
sibbl
40

Más de una solución alternativa, pero todos los navegadores modernos envían las cookies de dominio junto con la conexión, por lo que usan:

var authToken = 'R3YKZFKBVi';

document.cookie = 'X-Authorization=' + authToken + '; path=/';

var ws = new WebSocket(
    'wss://localhost:9000/wss/'
);

Termine con los encabezados de solicitud de conexión:

Cookie: X-Authorization=R3YKZFKBVi
Tim
fuente
1
Esta parece ser la mejor manera de pasar tokens de acceso en la conexión. En el momento.
cammil
2
¿Qué pasa si el URI del servidor WS es diferente del URI del cliente?
Danés
@ Danish Bueno, eso no funciona, ya que no puedes configurar cookies para otros dominios del lado del cliente
Tofandel hace
35

El problema del encabezado de autorización HTTP se puede abordar 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 usernamey password. Si necesita autorización básica, entonces está todo listo.


BearerSin embargo, quiero usar , 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 nombre de usuario no vacío y contraseña vacía, entonces interpreta el nombre de usuario como un token.

Dmitry Frank
fuente
12
Estoy probando la solución sugerida por usted. Pero no puedo ver el encabezado de autorización que se agrega a mi solicitud. Lo he intentado con diferentes navegadores, por ejemplo, Chrome V56, Firefox V51.0. Estoy ejecutando mi servidor en mi servidor local. entonces la url de websocket es "ws: // myusername: mypassword @ localhost: 8080 / mywebsocket". ¿Alguna idea de lo que podría estar mal? Gracias
LearnToLive
44
¿Es seguro transferir el token a través de la URL?
Mergasov
2
El nombre de usuario vacío / ignorado y la contraseña no vacía como token podrían ser mejores porque los nombres de usuario pueden registrarse.
AndreKR
9
Estoy de acuerdo con @LearnToLive: utilicé esto con wss (por ejemplo wss://user:[email protected]/ws) y no obtuve ningún Authorizationencabezado en el lado del servidor (con Chrome versión 60)
User9645
66
Tengo el mismo problema que @LearnToLive y @ user9645; ni Chrome ni Firefox están agregando el encabezado de autorización cuando el URI está en el wss://user:pass@hostformato. ¿No es esto compatible con los navegadores, o algo va mal con el apretón de manos?
David Kaczynski
19

No puede agregar encabezados pero, si solo necesita pasar valores al servidor en el momento de la conexión, puede especificar una parte de cadena de consulta en la url:

var ws = new WebSocket("ws://example.com/service?key1=value1&key2=value2");

Esa URL es válida pero, por supuesto, deberá modificar el código del servidor para analizarla.

Gabriele Carioli
fuente
14
es necesario tener cuidado con esta solución, la cadena de consulta puede ser interceptada, registrada en servidores proxy, etc., por lo que pasar información confidencial (usuarios / contraseña / tokens de autenticación) de esta manera no será lo suficientemente segura.
Nir
55
@Nir con WSS, la cadena de consulta probablemente debería estar segura
Sebastien Lorber
8
ws es texto sin formato. Usando el protocolo ws cualquier cosa puede ser interceptada.
Gabriele Carioli
1
@SebastienLorber no es seguro usar una cadena de consulta, no se está cifrando, lo mismo se aplica a HTTPS, pero como se usa el protocolo "ws: // ...", realmente no importa.
Lu4
55
@ Lu4, la cadena de consulta está encriptada, pero hay muchas otras razones para no agregar datos confidenciales como parámetros de consulta de URL stackoverflow.com/questions/499591/are-https-urls-encrypted/… & blog.httpwatch.com/2009 /
16

No puede enviar un encabezado personalizado cuando desea establecer una conexión WebSockets utilizando la API de JavaScript WebSockets. Puede usar Subprotocolsencabezados usando el segundo constructor de clase WebSocket:

var ws = new WebSocket("ws://example.com/service", "soap");

y luego puede obtener los encabezados de Subprotocols usando la Sec-WebSocket-Protocolclave en el servidor.

También hay una limitación, ¡los valores de los encabezados de Subprotocols no pueden contener una coma ( ,)!

Saeed Zarinfam
fuente
1
¿Puede un Jwt contener una coma?
CESCO
1
No lo creo JWT consta de tres cargas útiles codificadas en base64, cada una separada por un punto. Creo que esto descarta la posibilidad de una coma.
BillyBBone
2
Implementé esto y funciona, simplemente se siente raro. gracias
BatteryAcid
3
¿Está sugiriendo que usemos el Sec-WebSocket-Protocolencabezado como alternativa al Authorizationencabezado?
rrw
13

Enviar encabezado de autorización no es posible.

Adjuntar un parámetro de consulta de token es una opción. Sin embargo, en algunas circunstancias, puede ser indeseable 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á siendo registrado en cualquier lugar. Si esto plantea problemas de seguridad para usted, una alternativa es usar un token JWT secundario solo para el material del socket web .

Cree un punto final REST para generar este JWT , al que solo pueden acceder usuarios autenticados con su token de inicio de sesión principal (transmitido a través del encabezado). El socket web JWT 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 verificar el token de socket web en su base de datos (el JWT debe estar vinculado de alguna manera a su usuario en el back-end).

Norbert Schöpke
fuente
Esta fue la única solución segura que pude encontrar para los websockets de API Gateway. Slack hace algo similar con su API RTM y tienen un tiempo de espera de 30 segundos.
andrhamm
2

Totalmente pirateado así, gracias a la respuesta de kanaka.

Cliente:

var ws = new WebSocket(
    'ws://localhost:8080/connect/' + this.state.room.id, 
    store('token') || cookie('token') 
);

Servidor (usando Koa2 en este ejemplo, pero debería ser similar donde sea):

var url = ctx.websocket.upgradeReq.url; // can use to get url/query params
var authToken = ctx.websocket.upgradeReq.headers['sec-websocket-protocol'];
// Can then decode the auth token and do any session/user stuff...
Ryan Weiss
fuente
44
¿Esto no pasa su token en la sección donde se supone que su cliente debe solicitar uno o más protocolos específicos? Puedo hacer que esto funcione, no hay problema también, pero decidí no hacerlo y prefiero hacer lo que Motes sugirió y bloquear hasta que se envíe el token de autenticación en onOpen (). Sobrecargar el encabezado de solicitud de protocolo me parece incorrecto, y como mi API es para consumo público, creo que va a ser un poco confuso para los consumidores de mi API.
Jay
0

Mi caso:

  • Quiero conectarme a un servidor WS de producción a www.mycompany.com/api/ws...
  • utilizando credenciales reales (una cookie de sesión) ...
  • de una página local ( localhost:8000).

La configuración document.cookie = "sessionid=foobar;path=/"no ayudará ya que los dominios no coinciden.

La solución :

Añadir 127.0.0.1 wsdev.company.coma /etc/hosts.

De esta manera, su navegador utilizará cookies mycompany.comcuando www.mycompany.com/api/wsse conecte a un subdominio válido wsdev.company.com.

Max Malysh
fuente
-1

En mi situación (Azure Time Series Insights wss: //)

Utilizando el contenedor ReconnectingWebsocket y pude lograr agregar encabezados con una solución simple:

socket.onopen = function(e) {
    socket.send(payload);
};

Donde la carga útil en este caso es:

{
  "headers": {
    "Authorization": "Bearer TOKEN",
    "x-ms-client-request-id": "CLIENT_ID"
}, 
"content": {
  "searchSpan": {
    "from": "UTCDATETIME",
    "to": "UTCDATETIME"
  },
"top": {
  "sort": [
    {
      "input": {"builtInProperty": "$ts"},
      "order": "Asc"
    }], 
"count": 1000
}}}
Poopy McFartnoise
fuente
-3

Técnicamente, enviará estos encabezados a través de la función de conexión antes de la fase de actualización del protocolo. Esto funcionó para mí en un nodejsproyecto:

var WebSocketClient = require('websocket').client;
var ws = new WebSocketClient();
ws.connect(url, '', headers);
Jorge
fuente
3
Esto es para el cliente websocket en npm (para el nodo). npmjs.com/package/websocket En general, esto sería exactamente lo que estoy buscando, pero en el navegador.
arnuschky
1
Tiene un voto negativo porque este parámetro de encabezados está en la capa de protocolo de WebSocket, y la pregunta es sobre los encabezados HTTP.
Toilal
" headersdebe ser nulo o un objeto que especifique encabezados de solicitud HTTP arbitrarios adicionales para enviar junto con la solicitud". de WebSocketClient.md ; por lo tanto, headersaquí está la capa HTTP.
momocow
Además, cualquiera que desee proporcionar encabezados personalizados debe tener en cuenta la firma de la función del connectmétodo, descrita como connect(requestUrl, requestedProtocols, [[[origin], headers], requestOptions]), es decir, headersdebe proporcionarse junto con requestOptions, por ejemplo ws.connect(url, '', headers, null),. Solo la origincadena se puede ignorar en este caso.
momocow
-4

Puede pasar los encabezados como un valor clave en el tercer parámetro (opciones) dentro de un objeto. Ejemplo con token de autorización. Dejó el protocolo (segundo parámetro) como nulo

ws = new WebSocket ('ws: // localhost', nulo, {encabezados: {Autorización: token}})

Editar: Parece que este enfoque solo funciona con la biblioteca nodejs, no con la implementación estándar del navegador. Dejarlo porque podría ser útil para algunas personas.

Nodens
fuente
Tenía mis esperanzas por un segundo. No parece haber una sobrecarga tomando un tercer parámetro en WebSocket ctor.
Levitikon
Tengo la idea del código wscat. github.com/websockets/wscat/blob/master/bin/wscat línea 261 que utiliza el paquete ws. Pensé que este era un uso estándar.
Nodens