Form-Based Login for Java WebSocket Client

by
Tags: , ,

Needing a Java-based WebSocket client, I started with Tyrus, the reference implementation for the Java API for WebSocket. Writing a simple Java WebSocket client with Tyrus went well enough — until I went to turn security on. The Web App I’m connecting to uses form-based login, and out of the box the only authentication support is for HTTP BASIC or DIGEST. But with a little custom code, connecting via form-based login turned out to be possible.
Form-Based Login Mechanism
First, a quick review of how form-based login works:

  1. (Optional) The browser requests a protected page, and the server sends a 302 redirect to the login page
  2. The browser shows a login page or login area on the page, with a form including username and password fields
  3. The user completes the form, and their browser submits it to a login-processing URL
  4. Assuming the login was successful, the server sends either a 200 or a 302 redirect along with a cookie for the session ID. The redirect typically sends the user to either the protected page they originally requested, the page they were previously on, or a default landing page
  5. On future requests, the browser includes the session cookie so the server can identify the user.

Form-Based Login for WebSockets
To work with this for a WebSocket client, it has to complete the process like a normal browser would:

  1. Collect a username and password from the user
  2. Send the username and password to a login-processing URL
  3. Inspect the response (normally a 302) and extract the session cookie value
  4. Add the session cookie to the usual WebSocket client request
  5. (Optional) When the WebSocket is closed, call a logout-processing URL with the session cookie to invalidate it and allow the server to reclaim any session resources

Client Code
I began with a few Maven dependencies:

<dependency>
    <groupId>javax.websocket</groupId>
    <artifactId>javax.websocket-client-api</artifactId>
    <version>1.1</version>
</dependency>
<dependency>
    <groupId>org.glassfish.tyrus</groupId>
    <artifactId>tyrus-client</artifactId>
    <version>1.11</version>
</dependency>
<dependency>
    <groupId>org.glassfish.tyrus</groupId>
    <artifactId>tyrus-container-grizzly-client</artifactId>
    <version>1.11</version>
</dependency>

Writing the cookie into the WebSocket request is pretty straightforward. Starting from the Tyrus sample client, just replace this line:

final ClientEndpointConfig cec =
        ClientEndpointConfig.Builder.create().build();

with this block:

final ClientEndpointConfig cec =
        ClientEndpointConfig.Builder.create().configurator(
new ClientEndpointConfig.Configurator() {
  @Override
  public void beforeRequest(Map<String, List> headers) {
    super.beforeRequest(headers);
    String sessionId = login();
    List cookieList = headers.get("Cookie");
    if (cookieList == null) cookieList = new ArrayList();
    cookieList.add("SESSIONID=\""+sessionId+"\"");
    headers.put("Cookie", cookieList);
  }
}).build();

My back-end server uses Spring Security with the session cookie named SESSIONID, though yours may vary. You should be able to log in to the site with your favorite browser and inspect your cookies to determine the correct name of the session cookie.
That block of code puts the cookie into the WebSocket request. So then, we just need to perform the login and collection the correct session cookie value to write in there:

private static String login() {
  try {
    String loginBase = "http://somewhere.com/process_login";
    Console console = System.console();
    if(console == null) throw new RuntimeException("No console!");
    String username = console.readLine("Username: ");
    char[] password = console.readPassword("Password: ");
    URL loginUrl = new URL(loginBase+
        "?username="+username+"&password="+new String(password));
    HttpURLConnection con = (HttpURLConnection)
            loginURL.openConnection();
    con.setInstanceFollowRedirects(false);
    String cookie = con.getHeaderField("Set-Cookie");
    return cookie.replaceAll("SESSIONID=(.*?);.*", "$1");
  } catch (IOException | MalformedURLException e) {
    // do something sensible
    return null;
  }
}

This part is more complicated and the most likely to need some changes to work for you. Looking at some of the specific lines:

  • Line 3: This is the server URL that processes login form submissions. You can get this from the ACTION of the regular login form.
  • Lines 4-7: This example will run at the command line, and uses Console so it can capture a password without displaying it. You can collect the username and password however you like. If you try to run this directly from an IDE the Console class may not work, in which case you could use a wrapper around System.in instead.
  • Lines 9-10: My server lets you log in with a GET request. Others may require a POST request, which takes a little extra code to put the form data into the body of the HttpURLConnection (though not much). Be sure to use the correct names for the username and password parameters, which you can also get by inspecting the regular login form.
  • Line 14: Without this line, the HttpURLConnection will follow the 302 redirect that the server returns and produce the content for the subsequent page. The problem is, the second page won’t have the cookie header we need (plus, it’s just unnecessary traffic and delay). Better to stop at the initial response, so we disable redirect following.
  • Line 16: Once again, replace SESSIONID with the name of the session cookie for your app. The regular expression pulls the cookie value out of a cookie string that might look like this:
    SESSIONID=3BF34B40C42E7AEA3BC699AA512C4362; Path=/myapp/; HttpOnly
    

With that setup, the WebSocket request should supply the correct session cookie to show that it’s authenticated!
If you want to add the logout when the WebSocket connection is closed, just store the session ID generated by the login() method, and add a quick logout() method like this:

private String sessionId = ...; // Save from login routine
private static void logout() throws IOException {
  URL logout = new URL("http://somewhere.com/process_logout");
  HttpURLConnection con = (HttpURLConnection) logout.openConnection();
  con.setRequestProperty("Cookie", "SESSIONID=" + sessionId);
  con.setInstanceFollowRedirects(false);
  con.connect();
  sessionId = null;
}

Again, replace the logout URL and session cookie name with the appropriate values for your application.
For the Tyrus sample client, you can add a call to logout() just after the messageLatch.await() line.