WebSockets are a good technical solution where there is a requirement for interactive communication. A typical example is a chat system, but it makes much better sense for live updates such as the stock market. Being able to see share prices go from red to green is a “must have” for stock traders.
The WebSocket API is quite easy to use, but when it comes to security you don’t have a lot of options. However, don’t forget the first step in the WebSocket protocol is to upgrade a HTTP request. If you have a HTTP request, you should be able to use HTTP security mechanisms. Let’s try it out using the Basic security mechanism.
Note: As you may have guessed by its name, HTTP Basic authentication is very simple. It actually uses a very common pattern, which is to rely on a header for authentication so this solution could be generalized quite easily. For example, you could use the same technique with other header based authentication mechanisms, such as an OAuth bearer token.
A Look at WebSocket Implementation
To let us reuse HTTP security mechanism, we need to ensure we are authenticated before the WebSocket endpoint is activated. The WebSocket specification is implemented (in Tomcat, TomEE, WildFly, etc.) using a Servlet Filter
most of the time.
Unless you install another Filter
in the chain after the WebSocket Filter
, the WebSocket Filter
is implemented after all other Filter
, but before Servlet code is invoked. Generally, you want the Filter
providing security to be one of the first Filter
invoked, so this does not present as a constraint.
Note: In Tomcat and TomEE, it is also common to use a Valve
to replace the security Filter
to allow authentication to take place even before the Filter
chain. For instance, this is done automatically if you configure a login-config
in your web.xml
.
Everything is all ready to let us reuse Servlet container security. Let’s try it!
WebSocket Server Endpoint
For our example, we’ll write a simple WebSocket server endpoint sending “Hello <username>” when the session is opened.
To keep it simple we’ll use the annotation API:
@ServerEndpoint(value = "/socket")
public class TribeWebSocket {
@OnOpen
public void onOpen(final Session session) throws Exception {
session.getBasicRemote().sendText("Hello " + session.getUserPrincipal().getName());
}
}
Securing WebSocket using Basic Access Authentication
Now that we have an endpoint, let’s apply Basic access authentication security.
WebSocket (JSR 356, Java API for WebSocket) is clever in reusing most of the Servlet container security mechanisms. This means you can define a security-constraint
in your web.xml. Note however that only GET
http-method
applies to WebSocket endpoints.
Since our application is super complicated (a single class), we’ll secure the whole web application (/*
) but without having to define http-method
. However, if you want to define multiple security-constraint
to handle a different configuration by endpoint, you can still use url-pattern
configuration for servlets.
In term of role, we don’t need validation but we will want to authenticate users. To do this, we’ll use the meta-role **
as auth-constraint
, which just means “the user is logged.” As a final step, we can use Basic
authentication to define a login-config
with this auth-method
.
Ready to apply what was just explained about WebSocket? Lets securely lock it with Basic access authentication.
To secure WebSocket, standard servlets security works, but not that only GET
is supported as http-method
. To use Basic we will just add our web.xml
, which looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.1"
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd">
<security-constraint>
<web-resource-collection>
<web-resource-name>ws-security</web-resource-name>
<url-pattern>/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>**</role-name>
</auth-constraint>
</security-constraint>
<login-config>
<auth-method>BASIC</auth-method>
</login-config>
</web-app>
Note ** as role, means that we only want the user to be authenticated without any role constraint.
Java Client
Now our server secured, how can we connect to it with a WebSocket client? Only thing we need is to add the Authorization
header to the first request.
To do so, the WebSoclet JSR provides ClientEndpointConfig.Configurator
. This gives you the beforeRequest(header)
hook where you can add headers you want:
ClientEndpointConfig.Configurator configurator = new ClientEndpointConfig.Configurator() {
public void beforeRequest(Map<String, List<String>> headers) {
headers.put("Authorization", asList("Basic " + DatatypeConverter.printBase64Binary("user:password".getBytes())));
}
};
To pass the configurator to the client container we need to use an instance of ClientendpointConfig
. To make it easy to build we can use the associated builder:
ClientEndpointConfig clientConfig = ClientEndpointConfig.Builder.create()
.configurator(configurator)
.build();
Finally, we just need to connect to the server:
URI uri = new URI("...");
ClientEndpointConfig.Configurator configurator = new ClientEndpointConfig.Configurator() {
public void beforeRequest(Map> headers) {
headers.put("Authorization", asList("Basic " + DatatypeConverter.printBase64Binary("user:password".getBytes())));
}
};
ClientEndpointConfig clientConfig = ClientEndpointConfig.Builder.create()
.configurator(configurator)
.build();
WebSocketContainer container = ContainerProvider.getWebSocketContainer();
Session client = container.connectToServer(new MyEndpoint(), clientConfig, uri);
// do something
client.close();
If you prefer the annotation version, you’ll get something like:
@ClientEndpoint(configurator = AuthorizationConfigurator.class)
public class Client {
@OnMessage
public void onMessage(String content) {
// got a message
}
}
public class AuthorizationConfigurator extends ClientEndpointConfig.Configurator {
@Override
public void beforeRequest(Map> headers) {
headers.put("Authorization", asList("Basic " + printBase64Binary("Tomitribe:tomee".getBytes())));
}
}
Note: As you can see the issue with the annotation solution is that you can’t easily pass parameters to the Configurator
where it would be quite easy to do it with the programmatic API (and create a generic AuthorizationHeaderConfigurator
).
Testing Basic authentication for WebSocket
Didn’t believe it was that easy? To ensure it works, we’ll write a simple test: a client named “Tomitribe” will connect to the server endpoint and ensure he got “Hello Tomitribe” as the message.
To do so, we’ll simply use the brand new TomEE embedded JUnit rule for simplicity, but an Arquillian test would work as well. We’ll simply use the default setup, which is to deploy the Classpath as a WebApp. The only configuration we’ll do is to use a random port (to support parallel testing) and configure a user “Tomitribe” to be able to test our security.
All you need with this JUnit rule is to define it as public
in your test class:
@Rule
public TomEEEmbeddedRule server =
new TomEEEmbeddedRule(new Configuration().randomHttpPort().user("Tomitribe", "tomee"), "");
Then we just use a client close to the one we spoke about in previous part to connect, capture and validate the first message sent by the server:
@Test
public void sayHi() throws Exception {
final AtomicReference message = new AtomicReference<>();
final CountDownLatch latch = new CountDownLatch(1);
Endpoint endpoint = new Endpoint() {
@Override
public void onOpen(Session session, EndpointConfig config) {
session.addMessageHandler(new MessageHandler.Whole() {
@Override
public void onMessage(String content) {
message.set(content);
latch.countDown();
}
});
}
};
ClientEndpointConfig.Configurator configurator = new ClientEndpointConfig.Configurator() {
public void beforeRequest(Map> headers) {
headers.put("Authorization", asList("Basic " + printBase64Binary("Tomitribe:tomee".getBytes())));
}
};
ClientEndpointConfig authorizationConfiguration = ClientEndpointConfig.Builder.create()
.configurator(configurator)
.build();
Session session = ContainerProvider.getWebSocketContainer()
.connectToServer(
endpoint, authorizationConfiguration,
new URI("ws://localhost:" + server.getPort() + "/socket"));
latch.await(1, TimeUnit.MINUTES);
session.close();
assertEquals("Hello Tomitribe", message.get());
}
Tip: To ensure the endpoint is secured, you can write another test removing the following line:
headers.put("Authorization", asList("Basic " + printBase64Binary("Tomitribe:tomee".getBytes())));
And you’ll get the expected 401 exception which proves the server is secured:
javax.websocket.DeploymentException: The HTTP response from the server [HTTP/1.1 401 Unauthorized
] did not permit the HTTP upgrade to WebSocket
at org.apache.tomcat.websocket.WsWebSocketContainer.parseStatus
Give it a try – WebSocket secured by Basic authentication
If you want to play further with this post example, you can find the sources on GitHub at: https://github.com/tomitribe/secured-websocket.
Stay connected 😉