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.
I used this approach for dynamic websites where a secured section of the
site is used for administration of data. Rather than fighting HTML and
attempting to provide a slick administration interface using AJAX, I
decided there was no good reason not to use a Java Web Start application and
benefit from a Swing rich client. As some site administration was
already performed using web pages, a way of integrating the authentication of
web page based administration and rich client based administration was required
to prevent the need for multiple logins.
This article describes the solution used.
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.