DavidVine / amx-util-library

Various useful NetLinx include files and modules
MIT License
33 stars 11 forks source link

WebSocket Basic Authentication #3

Open jackbilesisdm opened 4 years ago

jackbilesisdm commented 4 years ago

Was doing a little but more work on this and i couldn't find an obvious and streamline way of Authenticating. So i've attached the bellow revision.

If the a username is given in the url string, the user and password are passed into httpBasicAuthentication to attach the relevant encoded data into the header. If a username is not given, the program should run as normal on an un-auth'd connection, and the uri is rebuilt from the data given in the string.

request.requestUri = "webSockets[idx].url.scheme,'://',webSockets[idx].url.host,webSockets[idx].url.path" //'ws://echo.websocket.org');

if(length_string(WebSockets[idx].url.user)) { // If username is given, Authentication is required by user
httpSetHeader(request.headers, 'Authorization',httpBasicAuthentication(WebSockets[idx].url.user,WebSockets[idx].url.password)); //"'JSESSIONID=','6wi46ymy1v9ss6yv289loli2'"); // 5wi46ymy1v9ss6yv289loli2
}

Would this be of some use or would would this conflict with usability in the future or for different endpoints on different RFC standards?

Full revision given below of the webSocketSendOpenHandshake function.

define_function webSocketSendOpenHandshake(dev socket) {
    HttpRequest request;
    char secWebSocketKey[200], host[200];
    integer idx;

    for(idx=1; idx<=length_array(webSockets); idx++) {
        if(socket == webSockets[idx].socket) {
            break;
        }
    }
    if(idx > length_array(wsSockets)) return;

    webSockets[idx].clientHandshakeKey = webSocketCreateHandshakeKeyClient();

    httpInitRequest(request);
    request.method = HTTP_METHOD_GET;
    request.version = 1.1;
    request.requestUri = "webSockets[idx].url.scheme,'://',webSockets[idx].url.host,webSockets[idx].url.path"//uriToString(webSockets[idx].url); //'ws://echo.websocket.org');
    if(length_string(WebSockets[idx].url.user)) { // If username is given, Authentication is required by user
        httpSetHeader(request.headers, 'Authorization',httpBasicAuthentication(WebSockets[idx].url.user,WebSockets[idx].url.password)); //"'JSESSIONID=','6wi46ymy1v9ss6yv289loli2'"); // 5wi46ymy1v9ss6yv289loli2
    }
    httpSetHeader(request.headers, 'Host', webSockets[idx].url.host);
    httpSetHeader(request.headers, 'Connection', 'Upgrade');
    httpSetHeader(request.headers, 'Upgrade', 'websocket');
    httpSetHeader(request.headers, 'Origin', "'http://',webSockets[idx].url.host");
    if(length_string(WebSockets[idx].sessionId)) {
        httpSetHeader(request.headers, 'Cookie', WebSockets[idx].sessionId); //"'JSESSIONID=','6wi46ymy1v9ss6yv289loli2'"); // 5wi46ymy1v9ss6yv289loli2
    }

    httpSetHeader(request.headers, 'Sec-WebSocket-Version', '13'); // version 13 is the standard as defined by RFC6455
    httpSetHeader(request.headers, 'Sec-WebSocket-Key', webSockets[idx].clientHandshakeKey);
    if(length_string(webSockets[idx].subprotocols)) {
        httpSetHeader(request.headers, 'Sec-WebSocket-Protocol', webSockets[idx].subprotocols);
    }
    print("'Sending HTTP WebSocket Open Handshake to Socket[',devToString(socket),']:'",false);

    print(httpRequestToString(request),true);

    webSockets[idx].readyState = CONNECTING;

    send_string socket, httpRequestToString(request);
}
DavidVine commented 4 years ago

Hi Jack,

Happy to update the websocket code to allow for a more streamlined authentication mechanism if this (a) works and (b) conforms to the RFC.

It's been a little while since I've played with the websocket code but I believe in all previous test and production code that I have implemented this in where authentication was required I initially connected to the webserver via standard HTTP (80) or HTTPS (443) and authenticated using whatever authentication mechanism was required (e.g., basic authentication). The web server then provided a session ID and I then used that session ID as a proof of auth token when opening the web socket - i.e., I didn't have to perform basic authentication again when sending the web socket open handshake request.

I'm just going off memory though and unfortunately I'm currently on leave with no access to internet other than my phone so I can't confirm right now or do any testing.

I just had a quick look at the Kaazing websocket test server which I used a lot during development and can see that it does provide an option to test username/password authentication. When I return to work - next week - I can have a look at this and extend functionality of the websocket code if this works.

DavidVine commented 4 years ago

Hi Jack,

Just had a quick read through RFC 6455 and it appears that the Authorization header can be included in the Open Handshake GET request. From section 4.1 (Opening Handshake, Client Requirements):

"The request MAY include any other header fields, for example, cookies [RFC6265] and/or authentication-related header fields such as the |Authorization| header field [RFC2616], which are processed according to documents that define them."

When I return to work next week I'll look at implementing this in the websocket include file.

jackbilesisdm commented 4 years ago

Hey David,

Thanks for the speedy responses! Let me know how your testing goes. Will leave open for now once you've confirmed everything you need to,

DavidVine commented 4 years ago

Hi Jack,

Have been playing around with today and it looks fairly trivial to do. I'm thinking of extending the webSocketOpen method to allow for an array of HttpHeader structures to be passed through. This way it more extensible and would allow for easy insertion of any HTTP headers required into the websocket handshake get request. Thoughts?

I am having trouble finding a websocket server that requires basic authentication within the websocket handshake. The Kaazing websocket echo test server (http://demos.kaazing.com/echo/) does have a websocket URL which uses basic auth (ws://demos.kaazing.com/echo-auth) but the Authorization header isn't included in the initial websocket handshake. Instead, it requires that you send another HTTP GET request (requesting the /echo-auth endpoint) containing the Authentication header. This second HTTP GET request is sent within the same TCP connection. E.g:

Websocket client sends:

GET wss://demos.kaazing.com/echo-auth HTTP/1.1
Host: demos.kaazing.com
Connection: Upgrade
Upgrade: websocket
Origin: http://demos.kaazing.com
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: RIQDOedPUKD44iUM+cPLLw==
Sec-WebSocket-Protocol: x-kaazing-handshake

To upgrade the connection from HTTP to websocket.

Kaazing websocket server responds:

HTTP/1.1 101 Web Socket Protocol Handshake
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: x-websocket-protocol
Access-Control-Allow-Origin: http://demos.kaazing.com
Connection: Upgrade
Date: Tue, 07 Jan 2020 06:47:57 GMT
Sec-WebSocket-Accept: uJMRe4L720ecT/xd6VNiVnvyExE=
Sec-WebSocket-Protocol: x-kaazing-handshake
Server: Kaazing Gateway
Upgrade: websocket

Indicating that the websocket is now open but because the client specified the x-kaazing-handshake subprotocol in the Sec-WebSocket-Protocol header further handshake is required to authenticate.

Websocket client sends to authenticate:

GET /echo-auth HTTP/1.1
Authorization: Basic dHV0b3JpYWw6dHV0b3JpYWw=

Kaazing websocket server responds:

HTTP/1.1 101 Web Socket Protocol Handshake
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: x-websocket-protocol
Access-Control-Allow-Origin: http://demos.kaazing.com
Connection: Upgrade
Date: Tue, 07 Jan 2020 06:47:58 GMT
Sec-WebSocket-Accept: uJMRe4L720ecT/xd6VNiVnvyExE=
Server: Kaazing Gateway
Upgrade: websocket

Which indicates that the websocket is now open, authenticated, and ready. At this point text, blob, array, or binary data can be sent to the websocket and HTTP is no longer required.

In the above example, if you put the Authorization header in the initial HTTP GET request it doesn't do anything and seems to just get ignored by the Kaazing websocket server. If you leave the Sec-WebSocket-Protocol header out of the initial websocket handshake GET request the server returns a 403 Forbidden error. If the client doesn't send a follow up GET request containing an Authorization header with valid basic auth then the server responds with a 401 Unauthorized.

Do you know of any websocket servers which do actually require the Authorization header in the initial websocket handshake?

jackbilesisdm commented 4 years ago

Hi David,

Sorry for the late reply, was working abroad last week.

Have been playing around with today and it looks fairly trivial to do. I'm thinking of extending the webSocketOpen method to allow for an array of HttpHeader structures to be passed through. This way it more extensible and would allow for easy insertion of any HTTP headers required into the websocket handshake get request. Thoughts?

This sounds fine to me as long as it can be documented somewhere in a README doc somewhere :)

I've never used Kaazing nor have i seen an auth / upgrade method in production environments. Might be used to get around some security features on their back-end, maybe?

But we've been using a NodeJS server using the ws library. Fairly simple to setup and i believe you can setup basic auth. Aslong as the Netlinx master and the server can see each other a network, this should work as intended. Maybe this is of some use for testing? If you need, i can share our NodeJS test script?

DavidVine commented 4 years ago

Thanks Jack.

Ah, I see. Apologies, I thought you had a pressing need to use basic authentication within the upgrade GET request of the websocket handshake.

I've seen authentication with websockets done a number of ways depending on the server. Sometimes a token obtained from one HTTP or websocket session is then used to authenticate another websocket - AMX NX masters do this. Sometimes, like with the Kaazing web server, a separate HTTP request/response is performed after the websocket has been established to authenticate.

I too have never seen an auth / upgrade method in production environment but that doesn't mean they might not be out there.

Out of interest, is this a requirement for your integration with the NodeJS server or are you just experimenting and looking at different ways to secure the connection between the NetLinx client-side websocket and the NodeJS webserver? Yes, please if you want to share your NodeJS test script that would be great :)

jackbilesisdm commented 4 years ago

We're using NodeJS to preview the output of the message sent by the master. The actual intended device doesn't have the best debugging information / menu.

const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', function connection(ws) {
  ws.on('message', function incoming(message) {
    console.log('received: %s', message);
  });

  //ws.send('something');
});

The above is C+P from the ws library README, seems to work for what we need, so we're not actually using the auth in the NodeJS environment. It appears that WS offers a similar solution with a HTTP request + upgrade to allow for authentication here

ircraigie commented 4 years ago

Quick note here that by definition a standard WebSocket connection does not support authentication or authorization - probably the reason the auth/upgrade method has not been observed in a production environment.

DavidVine commented 4 years ago

Hi @ircraigie ,

That's definitely something I've been trying to determine since @jackbilesisdm opened this ticket.

The RFC for the Websocket Protocol (https://tools.ietf.org/html/rfc6455) states:

The request MAY include any other header fields, for example, cookies [RFC6265] and/or authentication-related header fields such as the |Authorization| header field [RFC2616], which are processed according to documents that define them.

which seems to be add odds with your statement that "by definition a standard WebSocket connection does not support authentication or authorization". Are you able to provide details on where I can find that information in the standard? It would really help a lot and clear up the confusion.

ircraigie commented 4 years ago

Maybe I'm just propagating misinformation (again ;-)), but everything I have read suggest this is the case. For example here: Although the real culprit may just be that the standard javascript Websocket client implementation doesn't support the additional headers. There is a suggestion here that (deprecated) URL Basic Auth Info works - but not universally.

As you have already noted the most commonly cited implementation for Websocket authentication is retrieving a token or session ID through standard HTTP methods before hand then passing that parameter to the server immediately upon the upgrade. As I understand it the Kaazing protocol is/was(?) an attempt to create a standard for authentication inside the Websocket itself.

All that being said - adding support for a stack of generic headers in the http upgrade request seems a noble cause that could be particularly useful if one had ownership of the server.

DavidVine commented 4 years ago

Thanks @ircraigie Always great to have more than viewpoint! I think I'll go down the route of passing a group (array) of HttpHeader structs to the webSocketOpen function.

e.g., instead of this:

define_function webSocketOpen(dev socket, char url[], char subprotocols[], char sessionId[]) { ... }

have this:

define_function webSocketOpen(dev socket, char url[], HttpHeader headers[], char subprotocols[], char sessionId[]) { ... }

The webSocketOpen function calls either ip_client_open or tls_client_open but I need to wait for the socket to open before I can send the websocket UPGRADE HTTP GET request which will include the headers passed through to the HttpHeader array parameter. Because of the way wait statements work in NetLinx I can't just put a wait_until statement inside the webSocketOpen function to trigger sending the HTTP request when the socket is opened - otherwise subsequent calls to webSocketOpen will result in the values within the original wait statement being overwritten if it hasn't executed yet.

To achieve this I'll need to store the values of the HttpHeader array in a global variable, relative to the socket used, while inside the webSocketOpen function and then read those values back in the online event handler of the data_event for the socket. Most likely I'll just add an additional HttpHeader array member to the WebSocket structure. I don't think this will be a huge drain on volatile memory, which is plentiful, as HttpHeader struct uses 1124 bytes so even if I allow for 10 HttpHeader values in each of the 20 WebSocket structs which occupy the websockets array it's still only 224,800 bytes or 219.53KB - and the programmer can always up the count for more HttpHeader values per WebSocket struct if they need to.

ircraigie commented 4 years ago

Why break what's not broken?

Because they are very much the exception and no where near the rule maybe a secondary function call to add the ancillary headers?

define_function webSocketHeaders(dev socket, HttpHeader headers[])

Might need to call it before the open function which in turn could check to see if there are any headers staged for that socket.

DavidVine commented 4 years ago

Yep, good idea. I was actually thinking the same thing - don’t want people to download the latest update and then have their calls to webSocketOpen suddenly trigger compiler errors because they’re not passing a HttpHeader array.

jackbilesisdm commented 4 years ago

Hi David, apologies for not getting back on this.

Thanks for adding the label of enhancement for this one as it's not directly an issue or bug. I'm all for the headers array idea you have mentioned.

My direct intention of the above was to use a simple login sequence like you would with SSH

e.g username:password@192.168.0.1:8080, this for me made sense but it's up to you :)