Spring Security Integration
1 Introduction
How to Integrate the spring security framework in the eXo portal? This tutorial will guide you through a few steps and show you how easy it is to integrate spring security (or the Spring framework in general) in eXo portal. We will create a login portlet example as a support all along the document reading. The login portlet example has been developed and deployed using the eXo WCM product running on the application server JBoss 4.2.3. But it can easily be adapted to another eXo product (such as ECM) and to other servers such as tomcat. Moreover, the example, claiming to be a real world example, is implemented using JSF 1.2, the JBoss portlet bridge and Spring and can serve as a example project from where you can start your own portlet development targeting the eXo platform.2 Installation
This tutorial assumes that you have a working eXo WCM installation running under JBoss 4.2.x. Download the spring framework: http://s3.amazonaws.com/dist.springframework.org/release/SPR/spring-framework-2.5.6-with-dependencies.zip Download spring-security: http://sourceforge.net/project/showfiles.php?group_id=73357&package_id=270072&release_id=630203 Explode the 02portal.war file in the jboss server/default/deploy/exoplatform.sar directory and copy the following jars in WEB-INF/lib:- spring.jar
- spring-security-core.jar
- aspectjrt-1.5.4.jar
- exo-spring.jar (contains the filters and event handlers described in this tutorial - see attachment section of this page)
3 Configuration
To enable spring security in exo we need to go through a few configuration steps:3.1 JAAS disabling
First we need to disable the JAAS security which is the default authentication mechanism in exo. Edit 02portal.war web.xml file and comment out the JAAS configuration related lines:...
<session-config>
<session-timeout>15</session-timeout>
</session-config>
<!--
<security-constraint>
<web-resource-collection>
<web-resource-name>user
authentication</web-resource-name>
<url-pattern>/private/*</url-pattern>
<http-method>POST</http-method>
<http-method>GET</http-method>
</web-resource-collection>
<auth-constraint>
<role-name>users</role-name>
</auth-constraint>
<user-data-constraint>
<transport-guarantee>NONE</transport-guarantee>
</user-data-constraint>
</security-constraint>
<login-config>
<auth-method>FORM</auth-method>
<realm-name>exo-domain</realm-name>
<form-login-config>
<form-login-page>/login/jsp/login.jsp</form-login-page>
<form-error-page>/login/jsp/login.jsp</form-error-page>
</form-login-config>
</login-config>
-->
<security-role>
<description>a simple user role</description>
<role-name>users</role-name>
</security-role>
...3.2 Enabling spring security
To enable spring and set the spring security filter, add the following lines:...
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/security-context.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
......
<filter-mapping>
<filter-name>PortalContainerInitializedFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>SetCurrentIdentityFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
...3.3 security-context.xml
We need to configure the spring security filter chain for our purposes. Create a file named security-context.xml in 02portal.war WEB-INF directory containing the following lines:<?xml version="1.0" encoding="UTF-8"?> <beans:beans xmlns="http://www.springframework.org/schema/security" xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-2.0.1.xsd"> <http auto-config="true"> <intercept-url pattern="/private/**" access="ROLE_USER" /> <form-login login-page='/public/classic/Login' default-target-url='/private/classic/home' /> </http> <authentication-provider> <user-service> <user name="rod" password="koala" authorities="ROLE_SUPERVISOR, ROLE_USER, ROLE_TELLER" /> <user name="root" password="exo" authorities="ROLE_USER" /> </user-service> </authentication-provider> </beans:beans>
4 Login portlet example
Now that we have successfully installed and configured spring security in exo, we need a login portlet example to capture user credentials and serve as an entry point in the authentication process. The login portlet itself is based on JSF 1.2, Jboss portlet bridge and the spring framework, but you can obviously use whatever web framework you want to achieve the same.4.1 Building the portlet
So we need a login form to capture user credentials inputs. The portlet login form consists of the following lines of xml:<f:view xmlns:f="http://java.sun.com/jsf/core" xmlns:h="http://java.sun.com/jsf/html" xmlns:ice="http://www.icesoft.com/icefaces/component" xmlns:liferay-faces="http://liferay.com/tld/faces" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:c="http://java.sun.com/jstl/core" xmlns:fn="http://java.sun.com/jsp/jstl/functions" xmlns:t="http://myfaces.apache.org/tomahawk"> <style type="text/css" media="screen"> @import "/loginportlet/css/starter.css"; @import "/loginportlet/css/uni-form.css"; </style> <script src="/loginportlet/js/jquery.js" type="text/javascript"></script> <script src="/loginportlet/js/uni-form.jquery.js" type="text/javascript"></script> <h:form styleClass="uniForm" > <fieldset class="inlineLabels"> <legend>Sign in</legend> <div class="ctrlHolder"> <h:outputLabel for="login" style="width: 70px"><em>*</em>Login:</h:outputLabel> <h:inputText id="login" value="#{loginBean.login}" required="true" styleClass="textInput" /> <h:message for="login" styleClass="portlet-msg-error" /> </div> <div class="ctrlHolder"> <h:outputLabel for="password" style="width: 70px"><em>*</em>Password:</h:outputLabel> <h:inputSecret id="password" value="#{loginBean.passwd}" required="true" styleClass="textInput" /> <h:message for="password" styleClass="portlet-msg-error" /> </div> </fieldset> <div class="buttonHolder" style="margin-top: 20px; margin-right: 20px"> <h:commandButton styleClass="primaryAction" value="Submit" action="#{loginBean.login}" /> </div> </h:form> </f:view>
package org.exoplatform.loginportlet; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Controller; @Controller @Scope("request") public class LoginBean { String login; String passwd; public String login() throws Exception { String redirect = "/portal/j_spring_security_check?j_username=" + login + "&j_password=" + passwd; PortalUtils.sendRedirect(redirect); return null; } ... }
4.2 Setting up the login portal page
Now that we have a login portlet available we need to set it up into a portal page.- Log in as root in exo portal.
- Go to application registry and import the loginportlet
- Add a new hidden page named 'Login' under the portal classic's navigation (read more on page creation here). Make sure that the visible flag is unchecked to hide the page. Also declare the page as public so that everyone can access it without being authenticated for obvious reasons.
- Finally, drag & drop the login portlet in the page with the desired layout.
4.3 Customization of portal login and logout urls
In the portal header there is a login or logout action displayed depending whether you are already logged in or not. We need to customize those actions so that when the user clicks on it it will be redirect either to our login page or to the spring security logout url. Edit the article, go to the default.js tab and apply the following changes to the code:function validateUser() {
var user = eXo.env.portal.userName;
var rootObj = document.getElementById("classic-access");
var loginContentObj = eXo.core.DOMUtil.findFirstDescendantByClass(rootObj, "div", "UIWCMLoginPortlet");
var welcomeObj = eXo.core.DOMUtil.findFirstDescendantByClass(rootObj, "span", "Welcome");
var userObj = eXo.core.DOMUtil.findFirstDescendantByClass(rootObj, "span", "LoggedUser");
var languageObj = eXo.core.DOMUtil.findFirstDescendantByClass(rootObj, "a", "LanguageIcon");
var logXXXObj = eXo.core.DOMUtil.findPreviousElementByTagName(languageObj, "a");
if (user != "null") {
welcomeObj.innerHTML = "Welcome: ";
userObj.innerHTML = user;
logXXXObj.innerHTML = "Logout";
if (eXo.core.DOMUtil.hasClass(logXXXObj, "LoginIcon")) {
eXo.core.DOMUtil.removeClass(logXXXObj, "LoginIcon");
eXo.core.DOMUtil.addClass(logXXXObj, "LogoutIcon");
}
logXXXObj.onclick = function() { document.location.href = '/portal/j_spring_security_logout' }
} else {
if (eXo.core.DOMUtil.hasClass(logXXXObj, "LogoutIcon")) {
eXo.core.DOMUtil.removeClass(logXXXObj, "LogoutIcon");
eXo.core.DOMUtil.addClass(logXXXObj, "LoginIcon");
}
logXXXObj.innerHTML = "Login";
logXXXObj.onclick = function() { document.location.href = '/portal/public/classic/Login' };
}
languageObj.onclick = function () { if(document.getElementById('UIMaskWorkspace')) ajaxGet(eXo.env.server.createPortalURL('UIPortal', 'ChangeLanguage', true)); }
}
eXo.core.Browser.addOnLoadCallback("validateUser", validateUser);4.4 A look at the login page
Once you are done with all this, just click on the login action and you should be redirect to the login page looking something like that:
5 Integration strategies
Until now we haven't discussed about any integration strategies concerning a potential existing security realm outside of the eXo platform. To address this problem we have the choice between at least two different strategies:5.1 Direct integration
We can directly integrate eXo with the external realm. Everything related to organisation and user management in exo is cleanly separated in its own abstraction accessible through the OrganisationService. The authentication process itself is encapsulated in the Authenticator abstraction which sits on top of the organization service. eXo provides several implementations of both. So whether your realm is based on LDAP or JDBC and because the default implementations are generic enough, you will be able to use them and fits them to your needs with a matter of a little configuration. You can even develop a custom implementation to meet your more specific needs.5.2 Replication
Or we can go through a replication process between the external realm and the eXo platform realm. This is the strategy that we are going to use to build our login portlet example. Furthermore the replication will occur dynamically on any user authentication attempt.6 Integration with eXo portal
Being successfully authenticated against an external realm is not sufficient by itself. We also need to propagate the newly created security context to the portal own security mechanism. In eXo portal terminology, it means we have to create an Identity object for the user and register it into the Identity Registry. Spring framework provides a simple notification model where a bean can listen to application events published by other beans. Fortunately spring security uses this mechanism and publishes an InteractiveAuthenticationSuccessEvent in case of successful authentication. That will allow us to hook up custom code to that event. Furthermore, we need to replicate the user details from the external realm to the eXo portal one according to the integration strategy defined above. We create a SpringSecurityEventHandler bean that implements the ApplicationListener interface and listen to the InteractiveAuthenticationSuccessEvent event.package org.exoplatform.spring.security.web; ... public class SpringSecurityEventHandler implements ApplicationListener { private String portalContainerName = "portal"; public void onApplicationEvent(ApplicationEvent event) { if (event instanceof InteractiveAuthenticationSuccessEvent) { try { InteractiveAuthenticationSuccessEvent successEvent = (InteractiveAuthenticationSuccessEvent) event; ExoContainer container = getContainer(); String login = successEvent.getAuthentication().getName(); String passwd = successEvent.getAuthentication().getCredentials().toString(); IdentityRegistry identityRegistry = (IdentityRegistry) container.getComponentInstanceOfType(IdentityRegistry.class); Authenticator authenticator = (Authenticator) container.getComponentInstanceOfType(Authenticator.class); OrganizationService orgService = (OrganizationService) container.getComponentInstanceOfType(OrganizationService.class); User user = orgService.getUserHandler().findUserByName(login); if (user == null) { user = orgService.getUserHandler().createUserInstance(login); user.setFirstName(login); user.setLastName(login); orgService.getUserHandler().createUser(user, false); orgService.getUserHandler().saveUser(user, false); //TODO: put some more integration code here } Identity identity = authenticator.createIdentity(login); Subject subject = new Subject(); subject.getPrivateCredentials().add(passwd); subject.getPublicCredentials().add(new UsernameCredential(login)); identity.setSubject(subject); identityRegistry.register(identity); } catch (Exception e) { e.getMessage(); } } } protected ExoContainer getContainer() { // TODO set correct current container ExoContainer container = ExoContainerContext.getCurrentContainer(); if (container instanceof RootContainer) { container = RootContainer.getInstance().getPortalContainer(portalContainerName); } return container; } ... }
...
<beans:bean id="myEventHandler" class="org.exoplatform.spring.security.web.SpringSecurityEventHandler" />
...7 Security context propagation to portlets
Part of the problem is the question of security context propagation between on one side the portal webapp and at the other side the portlets webapps. This means that the security context has to be available in the portlet side allowing the application logic to deal with current user principal and granted authorities. By default Spring security uses a thread local variable to partially achieve this. But a problem may arise due to the fact that the portal invokes the portlet through a webapp cross context call. This means that it can lead to class cast exceptions (two different classloaders involved) or the security context simply not propagated at all. To accommodate this we will need to set up two request filters, one at the portal webapp side and the other at the portlet webapp side and use the http request to propagate the context in between.7.1 Portal side filter
We will use the spring security extensible filter chain to plug in our filter.package org.exoplatform.spring.security.web; ... public class PortalSideSecurityContextFilter extends SpringSecurityFilter { @Override protected void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { //fill request with security context SecurityContext context = SecurityContextHolder.getContext(); request.setAttribute(HttpSessionContextIntegrationFilter.SPRING_SECURITY_CONTEXT_KEY, context); //fill request with security last exception Object e = request.getSession().getAttribute(AbstractProcessingFilter.SPRING_SECURITY_LAST_EXCEPTION_KEY); request.setAttribute(AbstractProcessingFilter.SPRING_SECURITY_LAST_EXCEPTION_KEY, e); chain.doFilter(request, response); } public int getOrder() { // TODO Auto-generated method stub return 0; } }
...
<beans:bean id="myCustomFilter" class="org.exoplatform.spring.security.web.PortalSideSecurityContextFilter">
<custom-filter after="LAST" />
</beans:bean>
...7.2 Portlet side filter
In the portlet webapp we create a regular filter named PortletSideSecurityContextFilter.package org.exoplatform.spring.security.web; ... public class PortletSideSecurityContextFilter implements Filter { public void destroy() { } public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { Object object = request.getAttribute(HttpSessionContextIntegrationFilter.SPRING_SECURITY_CONTEXT_KEY); SecurityContext context = (SecurityContext) serializeDeserialize(object); if (context != null) { SecurityContextHolder.setContext(context); } else { SecurityContextHolder.clearContext(); } filterChain.doFilter(request, response); } public void init(FilterConfig arg0) throws ServletException { } private Object serializeDeserialize(Object obj) { Object result = null; try { ByteArrayOutputStream bout = new ByteArrayOutputStream(); ObjectOutputStream out = new ObjectOutputStream(bout); out.writeObject(obj); ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray()); ObjectInputStream in = new ObjectInputStream(bin); result = in.readObject(); } catch (Exception e) { //TODO: handle exception } return result; } }
...
<filter>
<filter-name>portletSideSecurityContextFilter</filter-name>
<filter-class>org.exoplatform.spring.security.web.PortletSideSecurityContextFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>portletSideSecurityContextFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>INCLUDE</dispatcher>
<dispatcher>FORWARD</dispatcher>
</filter-mapping>
...8 Conclusion
We are done! Now we know how to integrate the spring security framework in the eXo portal. Thanks to the the great integration capabilities of both eXo portal and Spring framework. You can have a look to the attachment section on this page and get the source code of this tutorial.
on 08/05/2009 at 09:48