Tuesday 14 August 2007

How to implement basic authentication with acegi - no container dependency

I have always implemented basic authentication by depending on a specific container implementation - always with the drawback of not being able to easy switch between containers in development, test and production.
Now I stood in front of another implementation and decided to look into Acegi's possibilities. If Acegi could provide a basic authentication mechanism bundled in my application I could get rid of the container dependency.
I did use Acegi on one other project to do form based login, but at that time the API changed between each little minor release making it a pain to upgrade.
Now they promise to keep the API stable so I thought it was about time give it a new fair chance to improve the first impression I got.

And I must say it did!

Following is a simple securityContext.xml needed to perform basic authentication using spring and acegi.
I provided a very simple UserDetailsService for the sake of the example. You should probably be using the
org.acegisecurity.userdetails.jdbc.JdbcDaoImpl implementation instead - or any other implementation that suites your setup.

securityContext.xml (download)

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">

<!-- ======================== FILTER CHAIN ======================= -->

<bean id="filterChainProxy"
class="org.acegisecurity.util.FilterChainProxy">
<property name="filterInvocationDefinitionSource">
<value>
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
PATTERN_TYPE_APACHE_ANT
/**=httpSessionContextIntegrationFilter,basicProcessingFilter,exceptionTranslationFilter,filterInvocationInterceptor
</value>
</property>
</bean>

<!-- responsible for setting up a security context holder for other
authentication mechanisms to work with -->
<bean id="httpSessionContextIntegrationFilter"
class="org.acegisecurity.context.HttpSessionContextIntegrationFilter">
<!-- no session should ever be created. We are processing basic
authentication credentials on each request -->
<property name="allowSessionCreation" value="false"/>
</bean>

<!-- Processes basic authentication headers and works together with the
provided authentication manager and
entry point to perform basic authentication -->
<bean id="basicProcessingFilter"
class="org.acegisecurity.ui.basicauth.BasicProcessingFilter">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="authenticationEntryPoint"
ref="authenticationEntryPoint"/>
</bean>

<!-- filter responsible for access decisions. What urlrequests may be
processed by which roles -->
<bean id="filterInvocationInterceptor"
class="org.acegisecurity.intercept.web.FilterSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="accessDecisionManager">
<ref local="httpRequestAccessDecisionManager"/>
</property>
<property name="objectDefinitionSource">
<value>
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
PATTERN_TYPE_APACHE_ANT
/**=USER
</value>
</property>
</bean>

<!-- filter responsible for translating exceptions and delegating to the
provided authenticationEntryPoint -->
<bean id="exceptionTranslationFilter"
class="org.acegisecurity.ui.ExceptionTranslationFilter">
<property name="authenticationEntryPoint"
ref="authenticationEntryPoint"/>
</bean>

<!-- ======================== AUTHENTICATION ======================= -->

<bean id="authenticationEntryPoint"
class="org.acegisecurity.ui.basicauth.BasicProcessingFilterEntryPoint">
<property name="realmName" value="Protected atlight"/>
</bean>

<bean id="authenticationManager"
class="org.acegisecurity.providers.ProviderManager">
<property name="providers">
<list>
<ref local="daoAuthenticationProvider"/>
</list>
</property>
</bean>

<bean id="daoAuthenticationProvider"
class="org.acegisecurity.providers.dao.DaoAuthenticationProvider">
<property name="userDetailsService" ref="simpleUserDetailsService"/>
</bean>

<!-- a simple simple UserDetailsService -->
<bean id="simpleUserDetailsService"
class="com.blogspot.ancientprogramming.security.SimpleUserDetailsService" />

<bean id="httpRequestAccessDecisionManager"
class="org.acegisecurity.vote.AffirmativeBased">
<property name="allowIfAllAbstainDecisions" value="false"/>
<property name="decisionVoters">
<list>
<ref local="roleVoter"/>
</list>
</property>
</bean>

<bean id="roleVoter" class="org.acegisecurity.vote.RoleVoter">
<property name="rolePrefix" value=""/>
</bean>
</beans>


web.xml
Together with this spring, acegi configuration in your web.xml you are up and running.
<!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>Archetype Created Web Application</display-name>

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

<filter>
<filter-name>Acegi Filter Chain Proxy</filter-name>
<filter-class>org.acegisecurity.util.FilterToBeanProxy</filter-class>
<init-param>
<param-name>targetClass</param-name>
<param-value>org.acegisecurity.util.FilterChainProxy</param-value>
</init-param>
</filter>

<filter-mapping>
<filter-name>Acegi Filter Chain Proxy</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
</web-app>

Sample (download)
I also provided a simple webapp demonstrating the implementation, just:
  1. download
  2. unzip
  3. type $mvn jetty:run
  4. go to: http://localhost:8080/basicAcegiExample
  5. type user, password as credentials

9 comments:

Anonymous said...

the download is not working

Anonymous said...

I must have zipped without recursion... sorry.

It should be working now.

Anonymous said...

I have blogged about a Form Based Acegi Solution if interested.

Toro said...

Very helpful, thank you.

Benoit Sanchez said...

Hello,

Everything works fine, my SimpleUserDetailsService get called, but when I enter the right login and password, I get a NullPointerException from acegi :

java.lang.NullPointerException
at org.acegisecurity.ui.ExceptionTranslationFilter.handleException(ExceptionTranslationFilter.java:229)
at org.acegisecurity.ui.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:176)
at org.acegisecurity.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:275)
at org.acegisecurity.ui.basicauth.BasicProcessingFilter.doFilter(BasicProcessingFilter.java:173)
at org.acegisecurity.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:275)
at org.acegisecurity.context.HttpSessionContextIntegrationFilter.doFilter(HttpSessionContextIntegrationFilter.java:249)
at org.acegisecurity.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:275)
at org.acegisecurity.util.FilterChainProxy.doFilter(FilterChainProxy.java:149)
at org.acegisecurity.util.FilterToBeanProxy.doFilter(FilterToBeanProxy.java:98)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:202)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:173)
at org.jboss.web.tomcat.filters.ReplyHeaderFilter.doFilter(ReplyHeaderFilter.java:96)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:202)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:173)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:213)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:178)
at org.jboss.web.tomcat.security.SecurityAssociationValve.invoke(SecurityAssociationValve.java:175)
at org.jboss.web.tomcat.security.JaccContextValve.invoke(JaccContextValve.java:74)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:126)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:105)
at org.jboss.web.tomcat.tc5.jca.CachedConnectionValve.invoke(CachedConnectionValve.java:156)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:107)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:148)
at org.apache.coyote.http11.Http11Processor.process(Http11Processor.java:869)
at org.apache.coyote.http11.Http11BaseProtocol$Http11ConnectionHandler.processConnection(Http11BaseProtocol.java:664)
at org.apache.tomcat.util.net.PoolTcpEndpoint.processSocket(PoolTcpEndpoint.java:527)
at org.apache.tomcat.util.net.MasterSlaveWorkerThread.run(MasterSlaveWorkerThread.java:112)
at java.lang.Thread.run(Unknown Source)

Can you help me ?

Benoit Sanchez said...

It's me again. I found the problem. It was that in my SimpleUserDetailsService, I did not give the UserDetails any GrantedAuthorities. Now it works fine.

Yet I have another question. As I saw in your securityContext, there should not be any session created. So that every request should be asking the credentials again. Though I disactivated all cookies in IE, I don't get the credential box when I click on links inside my site. Do you know why ?

Jacob von Eyben said...

Basic authentication relies on a header called 'Authorization' to contain the user credentials and verified on every request.
When you enters basic-authentication credentials in a browser, the browser remembers the credentials and it will send those along with every request.

That is why you won't be prompted for credentials when requesting other pages.

The no session setting will not prevent this.

Anonymous said...

thanks a lot

so difficult to find good examples which are complete and which work really, for Spring, AOP, EasyMock, Acegi, etc

with your code, at last I could make run a basic Spring program secured with Acegi

Anonymous said...

Yeah, that is a very good example. It really helped me out. Thanks!!!