How to Integrate Browser Authentication with a Java Web Start Application


This article describes a simple technique for integrating browser based authentication with a Java Web Start application that uses Acegi Security System for Spring and the Spring Framework's HttpInvoker to communicate to the server.

Approach Summary

The approach is based on passing the servlet session id into the Java Web Start application via a dynamicaly generated JNLP file. The Web Start application then appends the session id to HTTP requests to remote services that are protected by Acegi Security.

The scenario is as follows:

  • The user authenticates themselves through the browser.
  • The user requests the Java Web Start application from within the authenticated session.
  • The server dynamically generates the Web Start JNLP file for the client application providing the id of the session to the application as a system property.
  • For each security protected service that the client application calls over HTTP, the client appends the servlet session id to the URL of the request.

Dynamic Generation of JNLP File

The following code snippet shows a sample JSP file used to generate a JNLP file that can supply properties to a Web Start application for accessing secured remote services..

<%@ include file="/WEB-INF/jsp/includes.jsp" %>
<% response.setContentType("application/x-java-jnlp-file"); %>
<?xml version="1.0" encoding="utf-8"?>
<!-- JNLP File for Administration Tool -->
<jnlp
  spec="1.5+"
  codebase="http://<%= request.getServerName()%>/app"
  href="admin/adminApp.jnlp">
  <information>
    <title>Administration Tool</title>
    .........
  </information>
  <security>
      <all-permissions/>
  </security>
  <resources>
    <j2se version="1.5+" java-vm-args="-esa -Xnoclassgc "/>
    <property
      name="sid"
      value="$\{cookie['JSESSIONID'].value\}"/>
    <property
      name="serviceHost"
      value="<%=request.getServerName()%>"/>
    <jar href="myApp.jar"/>
  </resources>
  <application-desc
    main-class="com.mycompany.admin.AdminTool"/>
</jnlp>

The key part of the JNLP file are the properties passed into the JVM at the end of the example. The 'sid' property is the value of the JSESSIONID cookie for the current authenticated session and the 'serviceHost' property is the name of the server that hosts the JNLP file.

Supplying these two properties is enough to allow the client to make use of the authenticated browser session to call server-side services using HttpInvoker.

Remote Services

Using HttpInvoker it is very simple to expose services to remote clients. The example below shows two services that are to be used by the example client application.

<!--
  - Application context for remote services layer.
  -->
<beans>

  <bean
      name="/DirectoryService"
      class="org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter">
    <property
        name="service"
        ref="directoryService" />
    <property
        name="serviceInterface"
        value="com.mycompany.domain.logic.DirectoryService"/>
  </bean>

  <bean name="/IndexingService"
        class="org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter">
    <property
        name="service"
        ref="indexingService" />
    <property
        name="serviceInterface"
        value="com.mycompany.domain.logic.IndexingService"/>
  </bean>

</beans>

In this example, the beans are defined in a separate DispatcherServlet with its own context and a URL pattern of /remote*.

Securing Services

The remote services offered by the server-side application are secured using Acegi Security. The sample bean definition below shows how two URL paths are configured to accessable to users with the role, ROLE_ADMIN. In this example the URL paths in the application context starting with /admin (any web based admin screens are under this path) and /remote (any remote services are under this path).

<beans>
  .....
  <bean id="filterSecurityInterceptor"
        class="org.acegisecurity.intercept.web.FilterSecurityInterceptor">
    <property name="authenticationManager">
      <ref bean="authenticationManager"/>
    </property>
    <property name="accessDecisionManager">
      <ref bean="accessDecisionManager"/>
    </property>
    <property name="objectDefinitionSource">
      <value>
        CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
        \A/admin.*\Z=ROLE_ADMIN
        \A/remote.*\Z=ROLE_ADMIN
      </value>
    </property>
  </bean>
  .....
</beans>

With this example definition, both web based administration screens and remote services used by a rich client are secured and accessible only by users with the role, ROLE_ADMIN.

Accessing Remote services

In the Spring context of the client application, beans are defined to allow access to the remote services. In the example below, the bean definition required to access the remote indexing service is shown.

<beans>
  .....
  <!-- Remote indexing service -->
  <bean
    id="httpInvokerISProxy"
    class="com.mycompany.admin.SesssionHttpInvokerProxyFactoryBean">
    <property
      name="serviceUrl"
      value=
        "http://${serviceHost}/app/remote/IndexingService"/>
    <property name="httpInvokerRequestExecutor"
      ref="commonsHttpInvokerRequestExecutor"/>
    <property
      name="serviceInterface"
      value="com.mycompany.domain.logic.IndexingService"/>
  </bean>

  <bean
        id="indexingServiceClient"
        class="com.mycompany.admin.IndexingServiceClient"
        lazy-init="false">
    <property
      name="indexingService"
      ref="httpInvokerISProxy"/>
  </bean>

  <bean
    id="commonsHttpInvokerRequestExecutor"
    class="org.springframework.remoting.httpinvoker.CommonsHttpInvokerRequestExecutor"/>
  .....
</beans>

The important points to note in the above definitions are:

  • The Spring supplied HttpInvokerProxyFactoryBean class has been extended to allow the session id to be appended to every service request (see details below).
  • The serviceUrl passed to SesssionHttpInvokerProxyFactoryBean embeds the system property 'serviceHost' (i.e. the property passed to the client in the dynamically generated JNLP file)
  • Rather than using the default HttpInvokerRequestExecutor, the CommonsHttpInvokerRequestExecutor is used. This needs to be used because the default SimpleHttpInvokerRequestExecutor attempts to establish a new session each time a service is called rather than using the existing session identified by the session id appended to the service URL.

The code below for SesssionHttpInvokerProxyFactoryBean overrides HttpInvokerProxyFactoryBean setServiceUrl method to allow the jsessionid of the users current session to be appended to the service url. This allows service urls protected by Acegi Security to be accessed by the HttpInvoker client.

......

public class SesssionHttpInvokerProxyFactoryBean
    extends HttpInvokerProxyFactoryBean {

  private String sessionIdPostfix;

  public SesssionHttpInvokerProxyFactoryBean() {
    super();
    String jsessionid = System.getProperty("sid");
    if (jsessionid != null)
      sessionIdPostfix = ";jsessionid=" + jsessionid;
    else
      sessionIdPostfix = "";
  }

  public void setServiceUrl(String serviceUrl) {
    super.setServiceUrl(serviceUrl + sessionIdPostfix);
  }
}

Conclusions

The approach is simple to implement and deploy, and has worked successfully in practice.

Potential issues

Session timeout

If neither the Web Start client application or the browser based application are used within the timeout period of the session, the session expires and an error will occur the next time the application makes a request to a secured service. To re-establish a new session, the client application must be closed and restarted in a new session after re-authenticating in the browser.

Options to consider to reduce or resolve this problem include: (i) extending the length of the session timeout on the server; (ii) adding a scheduled 'keep-alive' request from the client to the server; (iii) adding logic to the client to enable it to establish a new session by re-requesting the users credentials.