Tuesday, June 17, 2008

Spring Security + Spring Remoting + Digest Authentication Part 2

Now for the server configuration. I will spare out the setup of a web application, since you will know that yourself.

First, the SecurityService and its implementation:

public interface SecurityService {
public String[] getRoles();
}



import org.springframework.security.GrantedAuthority;
import org.springframework.security.context.SecurityContextHolder;

public class SecurityServiceImpl implements SecurityService {
public String[] getRoles() {
GrantedAuthority[] gas = SecurityContextHolder.getContext().getAuthentication().getAuthorities();
String[] roles = new String[gas.length];
for (int i=0;i<gas.length; i++) {
roles[i] = gas[i].getAuthority();
}
return roles;
}
}


Not that complicated.
This is the web.xml, which looks rather straightforward:

<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
<display-name>web-deployed service layer</display-name>

<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext-service.xml</param-value>
</context-param>


<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>
org.springframework.web.filter.DelegatingFilterProxy
</filter-class>
</filter>

<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<servlet>
<servlet-name>context</servlet-name>
<servlet-class>
org.springframework.web.context.ContextLoaderServlet
</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet>
<servlet-name>remote</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<load-on-startup>2</load-on-startup>
</servlet>

<servlet-mapping>
<servlet-name>remote</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>


</web-app>



Following the convention, there is a [minimal] remote-servlet.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>

<bean name="/securityService"
class="org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter">
<property name="serviceInterface" value="de.yyy.xxx.SecurityService" />
<property name="service" ref="securityService" />
</bean>

</beans>



and finally, the biggest artifact, the server's application context.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:security="http://www.springframework.org/schema/security"
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.5.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-2.0.xsd">


<bean id="securityService" class="de.yyy.xxx.SecurityServiceImpl">
<security:intercept-methods>
<security:protect
method="de.yyy.xxx.SecurityServiceImpl.getRoles" access="ROLE_USER" />
</security:intercept-methods>
</bean>

<security:authentication-manager alias="authenticationManager" />

<bean id="digestProcessingFilter"
class="org.springframework.security.ui.digestauth.DigestProcessingFilter">
<property name="userDetailsService" ref="uds" />
<property name="authenticationEntryPoint"
ref="digestProcessingFilterEntryPoint" />
</bean>

<bean id="digestProcessingFilterEntryPoint"
class="org.springframework.security.ui.digestauth.DigestProcessingFilterEntryPoint">
<property name="realmName" value="ThisIsTheDigestRealm" />
<property name="key" value="acegi" />
<property name="nonceValiditySeconds" value="10" />
</bean>

<bean id="springSecurityFilterChain"
class="org.springframework.security.util.FilterChainProxy">
<security:filter-chain-map path-type="ant">
<security:filter-chain pattern="/**"
filters="httpSessionContextIntegrationFilter,digestProcessingFilter,exceptionTranslationFilter,filterSecurityInterceptor" />
</security:filter-chain-map>
</bean>

<bean id="httpSessionContextIntegrationFilter"
class="org.springframework.security.context.HttpSessionContextIntegrationFilter" />

<bean id="filterSecurityInterceptor"
class="org.springframework.security.intercept.web.FilterSecurityInterceptor">
<property name="authenticationManager"
ref="authenticationManager" />
<property name="accessDecisionManager"
ref="accessDecisionManager" />
<property name="objectDefinitionSource">
<security:filter-invocation-definition-source>
<security:intercept-url pattern="/**"
access="ROLE_USER" />
</security:filter-invocation-definition-source>
</property>
</bean>

<bean id="accessDecisionManager"
class="org.springframework.security.vote.AffirmativeBased">
<property name="allowIfAllAbstainDecisions" value="false" />
<property name="decisionVoters">
<list>
<bean
class="org.springframework.security.vote.RoleVoter" />
</list>
</property>
</bean>

<bean id="exceptionTranslationFilter"
class="org.springframework.security.ui.ExceptionTranslationFilter">
<property name="authenticationEntryPoint"
ref="digestProcessingFilterEntryPoint" />
</bean>

<security:authentication-provider>
<security:user-service id="uds">
<security:user name="jimi" password="jimi"
authorities="ROLE_USER, ROLE_ADMIN" />
<security:user name="bob" password="bob"
authorities="ROLE_USER" />
</security:user-service>
</security:authentication-provider>
</beans>



That's it. The client requests will be authenticated via the HTTP Digest algorithm,
the corresponding filter will set up a SecurityContext on the server side. When
the client asks for available roles, method security assures that only authorized clients can request them. The getRoles method then just accesses the SecurityContext and copies the rolenames in a String array which is returned.
There are some edges that need more polish, i.e. one could try to take the GrantedAuthority array and put this in a client's SecurityContext. With some more handcoding it should be possible to send the SecurityContext over the wire, but it needs more analysis if this makes sense.

4 comments:

Anonymous said...

Thanks..

Karsten Gresch said...

This very valuable information!

IMHO this ***is*** an elegant solution for loosely coupled Rich Clients as it circumvents common problems when dealing with multithreading (e.g. SwingWorker).

Here approaches like http://forum.springframework.org/showthread.php?t=54036 don't work well as you need to bypass the Authentication to every thread source and add it to the SecurityContext. This would break the principle understanding security as a crosscutting concern.

B.t.w. - global method security also works with your approach.

<security:global-method-security access-decision-manager-ref="accessDecisionManager">
<security:protect-pointcut access="ROLE_ADMIN" expression="execution(* zz.yyy.xxx.services..*.*(..))"/>
</security:global-method-security>

Thanks Elvis from hell!!!

Karsten

Alex said...

Thanks Elvis,

I agree with the previous poster. I was trying to accomplish exactly the same thing in my own app and your post probably saved me at least half a day of trying to string this together myself!

I only added hashing the password instead of saving it in clear text. Since I could'nt get Spring's ShaPasswordEncoder to work properly, I gave up and implemented my own.

Elvis from hell said...

yeah, I'll configure for GMS the next refactoring session.

The password stuff was left out by me here since it's just another topic. Here, I was storing it hashed in the database, but now I don't have any because I just pass it through to our Domain Controller. (yes, windows, it's not in my hands :)) One password less to store and to remember.

When I am developing a software with an own password store, I usually do the hashing already on client side. Since people tend to reuse their passwords on different sites, sending only the hash over the wire (opposed to the cleartext password) adds a little more security, should the channel be insecure.