Snippet 0x08: HTTP Basic Auth for secure WebSocket connections (with Undertow)
For my open source file sync software Syncany, I use the embedded web server and web socket server Undertow to provide a websocket and REST based interface by the Syncany daemon. Syncany clients (such as the GUI, or potentially a web interface) connect to this daemon, send requests and receive asynchronous events. Syncany’s GUI client also uses the Undertow websocket client to connect to the above mentioned daemon.
To authenticate the websocket client with the daemon, the simple HTTP basic authentication mechanism over HTTPS is used. This tiny post shows you how to authenticate against a websocket server with HTTP basic auth using the Undertow websocket client.
1. HTTP basic for normal web pages
The HTTP basic authentication is widely used across the Internet for normal HTTP(S) web pages, but rarely used to authenticate websocket clients — at least not to my knowledge. A reason for this could be that some browsers still struggle with handing over the HTTP basic “Authorization” header to the actual HTTP-based websocket handshake. Firefox supports this, Chrome does not — I don’t know about IE. For Syncany, we have decided to use HTTP basic authentication anyway, because it is very simple to implement and is equally secure as other methods if HTTPS is enforced.
2. HTTP basic for websockets
The entire “magic” behind HTTP basic is to ensure that all HTTP requests contain a HTTP header called Authorization. This header contains the authorization method (in this case Basic) and the user name and password of the logged in user in the format base64(username:password).
For normal HTTP requests, this looks like this:
1 2 3 |
GET /hello.html HTTP/1.1 Host: www.example.com Authorization: Basic d2lraTpwZWRpYQ== |
For websocket communication, there is only one HTTP request: The handshake that leads to the protocol upgrade. And that is precisely where we have to add this header. Assuming that our websocket server listens on 127.0.0.1 with the endpoint at /api/ws
1 2 3 4 5 6 7 8 |
GET /api/ws HTTP/1.1 Host: 127.0.0.1 Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw== Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13 Authorization: Basic d2lraTpwZWRpYQ== |
The websocket server will use the base64 encoded username and password to authenticate the use and only send a valid successful response (HTTP/1.1 101 Switching Protocols) if it succeeds. If not, it will send an error message (HTTP/1.1 401 Unauthorized).
3. Add “Authorization” header to the websocket handshake
To achieve this using the Undertow websocket client (WebSocketClient and WebSocketChannel), all we have to do is to manually pass the above mentioned Authorization header in the HTTP-based handshake.This can be done by passing a WebSocketClientNegotiation object with a WebSocketExtension to the call of WebSocketClient.connect(). The full code is available in the Syncany GUI plugin:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
SSLContext sslContext = ... Xnio xnio = Xnio.getInstance(this.getClass().getClassLoader()); Pool<bytebuffer> buffer = new ByteBufferSlicePool(BufferAllocator.BYTE_BUFFER_ALLOCATOR, 1024, 1024); OptionMap workerOptions = OptionMap.builder() .set(Options.WORKER_IO_THREADS, 2) .set(Options.WORKER_TASK_CORE_THREADS, 30) .set(Options.WORKER_TASK_MAX_THREADS, 30) .set(Options.SSL_PROTOCOL, sslContext.getProtocol()) .set(Options.SSL_PROVIDER, sslContext.getProvider().getName()) .set(Options.TCP_NODELAY, true) .set(Options.CORK, true) .getMap(); XnioWorker worker = xnio.createWorker(workerOptions); XnioSsl xnioSsl = new JsseXnioSsl(xnio, OptionMap.create(Options.USE_DIRECT_BUFFERS, true), sslContext); URI endpointUri = new URI("wss://localhost:8443/api/ws"); WebSocketClientNegotiation clientNegotiation = new WebSocketClientNegotiation( new ArrayList<string>(), new ArrayList<websocketextension>()) { @Override public void beforeRequest(Map<string, string=""> headers) { String basicAuthPlainUserPass = daemonUser.getUsername() + ":" + daemonUser.getPassword(); String basicAuthEncodedUserPass = Base64.encodeBase64String( StringUtil.toBytesUTF8(basicAuthPlainUserPass)); headers.put("Authorization", "Basic " + basicAuthEncodedUserPass); // <<< This is where the magic happens } }; webSocketChannel = WebSocketClient.connect(worker, xnioSsl, buffer, workerOptions, endpointUri, WebSocketVersion.V13, clientNegotiation).get();</string,></websocketextension></string></bytebuffer> |
The code actually looks more complicated than it is, because the XNIO worker (was at: http://undertow.io/documentation/core/listeners.html, site now defunct, July 2019) is configured to support SSL/TLS.
A. About this post
I’m trying a new section for my blog. I call it Code Snippets. It’ll be very short, code-focused posts of things I recently discovered or find fascinating or helpful. I hope this helps
I want to add Authorization header for basic auth to my jetty-client request ,I am running two instances on same browser one is server one is client ,using org.eclipse.jetty ,jetty-websocket,8.1.15.v20140411 .had a WebsocketEndPointManager class which contains the initialisation method for both Server(dedicated class is CSWebsocket) and client(Cpwebsocket).
How can I achieve basic auth in my scenario Server Instance maintain username and password in Session after it starts ,currently my setup able to perform upgrade to websocket but not basic auth.
Snippet of client method where handshake occur :
private void startCPWebSocket() throws Exception{
String uri =””;
try {
HandlerRegistry.register(this);
WebSocketClientFactory factory = new WebSocketClientFactory();
Configuration configuration = TestSession.getConfiguration();
uri = configuration.getSystemUnderTestEndpoint();
TestSession.getConfiguration().setToolURL(“NA”);
TestSession.getConfiguration().setSystemUnderTestEndpoint(uri);
factory.start();
WebSocketClient client = factory.newWebSocketClient();
client.setMaxIdleTime(24*60*60*1000);
client.open(new URI(uri.trim()), new CPWebSocket()).get(24*60*60,
TimeUnit.SECONDS);
LogUtils.logInfo(“WebSocket URL : “+uri,true);
}
} catch (ExecutionException e) {
LogUtils.logError(e.getMessage(), e,false);
LogUtils.logWarn(“Could not establish websocket connection.”,true);
//System.exit(0);
TestSession.setTerminated(true);
throw e;