What is the recommended way to secure a WS connection?
Predix Websocket Client: How does it push messages from the secure target WebSocket to the browser and let the client JavaScript know?
Predix Websocket Client: If the connection to Predix WebSocket Client is established through REST who then creates a WS relay connection, how does the client JavaScript know about, connect to, and manage the WS relay connection?
Use a server-side relay to connect securely (Predix Websocket Client)
On Websocket open, pass token in headers
On Websocket open, pass in tokens
Manually create HTTP WS Upgrade request
Secure messages with tokens
Are there any other options?
Since server-side technology can send headers (with tokens) when establishing a secure connection, then a browser could connect to an insecure service who then securely connects to the target WebSocket. While it is possible to write this code in Node.js or Java, why not use the Predix Websocket Client template?
Not possible from Browser.
new WebSocket(url, protocol)
does not provide an interface for headers to be sent.
Would secure the WebSocket (if it works)
Not supported across all browsers
Not recommended
Browser method for creating a WebSocket connection is new WebSocket(url, protocol)
. url
can be in the format of either ws://somehost.com/resource
, it could also include username and password in this format: ws://username:password@somehost.com/resource
. While it is possible to shim a custom string in place of the username and password, I am unsure if our token length would result in exceeding the url length limit.
Does not work.
Technically, new Websocket()
performs a HTTP GET request on the same endpoint with an "upgrade" header, upon successful handshake, the socket connection is then permitted. In theory, headers could be sent with this HTTP request, but in practice, the interface for new WebSocket()
doesn't allow them. If you try to set headers, they are rejected.
The WebSocket Spec
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
If you try to make an XMLHttpRequest with the WebSocket headers, it rejects the request because those headers can't be sent. I confirmed this using Polymer Iron-Ajax and XMLHttpRequest.
Does not work:
var xhr = new XMLHttpRequest();
xhr.addEventListener('load', handleLoad);
xhr.open("GET", 'ws://somehost/livestream');
xhr.setRequestHeader('authorization', 'bearer someverylongstringthathasbeenshortened');
xhr.setRequestHeader('tenant', 'some-tenant-string-goes-here');
xhr.setRequestHeader('Upgrade', 'websocket');
xhr.setRequestHeader('Connection', 'Upgrade');
xhr.setRequestHeader('Sec-WebSocket-Key', 'SeCREtKeY==');
xhr.setRequestHeader('Sec-WebSocket-Protocol', 'chat, superchat');
xhr.setRequestHeader('Sec-WebSocket-Version', '13');
xhr.send();
function handleLoad (res) {
document.querySelector('#output').innerText = xhr.responseText;
}
This will also error out because Chrome performs a pre-fetch OPTIONS method request before any GET request. This OPTIONS method is rejected on WS endpoints without specific configuration
Open a WebSocket connection without any security, but then pass a token with each subscription request. No data will be sent to client unless token validates.
Answer by Evan Dana · May 11, 2017 at 11:29 AM
Summary of conversation with @Greg Stroup:
Take the server code from Security Starter's web server and turn into a Secure WS Relay repo that will enable the following:
UI connects securely to WS Relay through HTTPS using headers with tokens
WS Relay then opens a secure connection to the Target WS Endpoint
WS Relay responds to UI with socket ID of WS Relay - target endpoint connection
UI establishes WS connection (insecurely) to WS Relay with standard new WebSocket()
UI socket messages include socket ID
Upon receiving socket messages from UI, WS Relay only forwards messages with matching socket ID
Insecure WS to WS Relay
Secure WS Relay to Target WS Endpoint
Add additional security on REST WS Relay secure connection with PassportJS
Example: https://github.build.ge.com/AutomationNorth/hmi-microapp/blob/develop/server.js#L63
UI connects to Server code who connects to service
Service is now protected from DDOS attacks because UI server would fail first. This is preferable because service downtime could corrupt data integrity.
Reverse Proxy Requests and Handle WS Differently
/**
* Reverse proxy HTTP requests on the given endpoint. If proxyWs is true then it will also listen to upgrade requests on
* that endpoint and proxy those too.
* @param endpoint The endpoint where this reverse proxy should listen (ie. /my-reverse-proxy)
* @param url The URL where these requests should be proxied to
* @param proxyWs Whether this endpoint should also reverse proxy websocket connections
*/
let doReverseProxy = (endpoint, url, proxyWs) => {
console.log('Proxying requests to', endpoint, '->', url, proxyWs === true ? '(including websockets)' : '');
let curProxy = reverseProxy.create(url, { ws: proxyWs === true });
// proxy HTTP requests as express middleware
app.use(endpoint, (req, res) => {
curProxy.web(req, res);
});
curProxy.on('error', (err) => {
console.warn('Error while proxying request to', url, err);
});
if (proxyWs === true) {
// listen to upgrades and proxy them too if they come from the right place
server.on('upgrade', (req, socket, head) => {
if (req.url.startsWith(endpoint)) {
// since websocket requests aren't getting caught by the middleware their URL still has the reverse proxy endpoint
// on it. so we need to turn /my-reverse-proxy/some-url into /some-url for it to be reverse proxied correctly
req.originalUrl = req.url;
req.url = req.url.substring(endpoint.length);
curProxy.ws(req, socket, head);
}
});
server.on('error', function (err, req, socket) {
console.warn('Server error on socket', url, err);
console.warn('arguments', arguments);
socket.end();
});
}
};
Pass Along Headers
const httpProxy = require('http-proxy');
const HttpsProxyAgent = require('https-proxy-agent');
let options = {
changeOrigin: true,
};
// if a proxy is set then use it in the connection
let proxyServer = process.env.HTTPS_PROXY || process.env.HTTP_PROXY;
if (proxyServer) {
console.log('Configuring the reverse proxy to make its requests through the following HTTP proxy:', proxyServer);
options.agent = new HttpsProxyAgent(proxyServer);
}
// if we're running locally look to the environment for token/tenant info
options.headers = {};
if (process.env.DEBUG_TOKEN) {
options.headers.Authorization = 'Bearer ' + process.env.DEBUG_TOKEN;
}
if (process.env.TENANT_ID) {
options.headers.tenant = process.env.TENANT_ID;
}
/**
* Creates a reverse proxy on the given URL with the given options.
* @param url The URL to proxy things to
* @param otherOptions The options to use. These will supplement the default options at the top of this module.
* @returns {*|Object} An http-proxy object
*/
let createReverseProxy = (url, otherOptions) => {
let myOptions = otherOptions || {};
let properties = Object.getOwnPropertyNames(options);
for (let i = 0; i < properties.length; i++) {
let property = properties[i];
if (!myOptions.hasOwnProperty(property)) {
myOptions[property] = options[property];
}
}
myOptions.target = url;
return httpProxy.createProxyServer(myOptions);
};
module.exports = {
create: createReverseProxy,
};
Go to Client Login, then API Explorer to preview how this is set up
Underlying code for this example: https://github.com/PredixDev/security-starter
Uses Predix Websocket Server and Webapp Starter
UI starter app that connects to Predix Websocket Server
Uses Predix Websocket Client
Note: Still unsure how to configure the Websocket Client through the Websocket Server
Predix Websocket Client: https://github.com/PredixDev/predix-websocket-client
Answer by Tom Turner · May 10, 2017 at 06:16 PM
You can look at how predix-websocket-client makes a secure connection to Predix Timeseries, then imagine how it's implemented on the Time Series WSS side.
Essentially, Time Series is tied to UAA. You pass a UAA Token to the WSS endpoint in the Header when making the connection. On the server side, Timeseries checks that token with the same UAA and if it matches up, then it lets you create the WS Connection.
Answer by Greg Stroup · May 10, 2017 at 02:45 PM
Good question. You're on the right track. I think "Use a server-side relay to connect securely" is the way to go. But I believe predix-websocket-client is really just a client. It's only part of what you need. Did you look at the predix-websocket-server repo? (It uses the predix-websocket-client.) I think that's more like what you need.
https://github.com/PredixDev/predix-websocket-server
The Predix Event Hub service would also meet your needs. You could configure it to talk to your back end websocket, then browsers could subscribe to it for events. (Haven't done this myself, but I think it should work.)
@Susheel Choudhari @Tom Turner any insights?