Acegi on Grails

Acegi on Grails

Please note: This tutorial is very outdated. The acegi plugin and grails have changed and matured making the information here misleading.

Here is the current tutorial: http://docs.codehaus.org/display/GRAILS/AcegiSecurity+Plugin+-+Basic+Tutorial (4-2008)

Tutorial to use "grails domain class " from the Acegi security.
You can Download Sample Project for this Tutorial acegilogon.zip based on Grails 0.3 SNAPSHOT

Plugin

Description

Tutorial to use "grails domain class " from the Acegi security. and Securing your applications with "Acegi filters".
I referred the article of Blog "Securing a Grails application using Spring and Acegi Security" and Reference Guide of Acegi.

Implementation overview:

  1. Domain class to use for authentication and authorization.
  2. Controller for Login and Logout.
  3. Class that implements UserDetailsService to use User Domain Class from Grails Application.
  4. Class that implements FilterInvocationDefinitionSource to use Request Map from Grails Application.
  5. Abstract class that extends WebApplicationObjectSupport to get GrailsApplication.
  6. Acegi Configuration.
  7. web.xml Configuration.

Steps

    Note: All paths are relative from your projects home directory.

  1. run grails create-app to create your application.
  2. Download and Copy Needed libraries to lib/ see
  3. run the target grails create-domain-class and create domain classes needed for logins (Person,Authorities,Requestmap). see
  4. run the target grails create-controller and create Login and Logout Controllers. see
  5. create auth.gsp grails-app/views/login/auth.gsp . see
  6. Download Java source and copy to src/java/grails/ . see
    grails.GrailsWebApplicationObjectSupport.java
    grails.GrailsDaoImpl.java
    grails.GrailsFilterInvocationDefinition.java
  7. Copy securityContext.xml to web-app/WEB-INF/ see
    securityContext.xml
  8. edit web.xml . see
  9. Edit grails-app/conf/BootStrap.groovy file to add sample data. see
  10. run your Grails application

Files Overview

Controller,Domain Class,View

  • %PROJECT_HOME%/grails-app/controllers/LoginController.groovy - main controller for login
  • %PROJECT_HOME%/grails-app/controllers/LogoutController.groovy - logout controller
  • %PROJECT_HOME%/grails-app/domain/Person.groovy - Domain class of Login users
  • %PROJECT_HOME%/grails-app/domain/Authorities.groovy - Domain class of Authorities
  • %PROJECT_HOME%/grails-app/domain/Requestmap.groovy - Domain class of Request Map
  • %PROJECT_HOME%/grails-app/views/login/auth.gsp - Example login page

Needed libraries

Acegi Bean Configuration file

  • %PROJECT_HOME%/web-app/WEB-INF/securityContext.xml
  • %PROJECT_HOME%/web-app/WEB-INF/web.template.xml

Java codes that implements the Acegi classes

  • %PROJECT_HOME%/src/java/grails/GrailsWebApplicationObjectSupport.java
  • %PROJECT_HOME%/src/java/grails/GrailsFilterInvocationDefinition.java
  • %PROJECT_HOME%/src/java/grails/GrailsDaoImpl.java

Database & Tables (Domain Class)

Person.groovy
class Person {
	def Long id;
	def Long version;

	String username
	String passwd
	boolean enabled

	def constraints = {
		username(blank:false,unique:true)
		passwd(blank:false)
		enabled()
	}
}
Authorities.groovy
class Authorities {
	def Long id;
	def Long version;

	String username
	String authority
	def constraints = {
		username(blank:false)
		authority(inList:["ROLE_SUPERVISOR", "ROLE_USER"] )
	}
}
Requestmap.groovy
class Requestmap {
	def Long id;
	def Long version;

	String url
	String config_attribute

	def constraints = {
		url(blank:false,unique:true)
		config_attribute(blank:false)
	}
}

Controllers and View

LoginController.groovy
import org.acegisecurity.Authentication as Auth
import org.acegisecurity.context.SecurityContextHolder as SCH
import org.acegisecurity.providers.anonymous.AnonymousAuthenticationToken
import org.acegisecurity.providers.AbstractAuthenticationToken

class LoginController {

	def index = {
		def auth = SCH.getContext().getAuthentication();
		if (auth != null) {
			if(auth instanceof AnonymousAuthenticationToken){
				redirect(action:"auth")
			}else if(auth instanceof AbstractAuthenticationToken){
				redirect(uri:"/")
			}else{
				redirect(action:"auth")
			}
		}else{
			redirect(action:"auth")
		}
		render(text:"login")
	}

	def auth = {}

	def denied ={
		render(text:"access denied")
	}

	def authfail = {
		println params
		render(text:"authfail")
	}

	/**
	 * load user and return domain class called from GrailsFilterInvocationDefinition
	 */
	def loadUserByUsername(username){
		Person.findByUsername(username)
	}

	/**
	 * load authorities by username
	 */
	def authoritiesByUsername(username){
		Authorities.findAllByUsername(username)
	}

	/**
	 * load request map
	 */
	def requestMap(hql) {
		 //"from Requestmap where url = '/**' or url ='/book/**' or url = '/book/list/**' order by length(url) desc"
		Requestmap.findAll(hql)
	}
}
sample login form auth.gsp
<html>
	<head>
		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
		<meta name="layout" content="main" />
		<title>Login</title>
	</head>
	<body>
		<div class="body">
			<form action="../j_acegi_security_check" method="POST">
				<p>
					<label for="person_id">Login ID</label>
					<input type='text' name='j_username' >
				</p>
				<p>
					<label for="pwd">Password</label>
					<input type='password' name='j_password'>
				</p>
				<p>
					<input type="checkbox" name="_acegi_security_remember_me">Remember me 2 weeks<br/>
					<input type="submit" value="Login" />
				</p>
			</form>
		</div>
	</body>
</html>

For Logout

LogoutController.groovy
class LogoutController {
	def index = {
		redirect(uri:"/j_acegi_logout")
		render(text:"")
	}
}

Edit web.xml

Add the following line to web.xml file located in the src/templates/war directory.  If you don't have this directory you should run the command "grails install-templates" to get it.

add a param-value to the <context-param> with the name "contextConfigLocation" like below:

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

add a new filter before the existing Sitemesh <filter> (the filter you're adding should be the first filter listed).

<filter>
  <filter-name>acegiAuthenticationProcessingFilter</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>

add a new filter mapping before the charEncodingFilter filter mapping (the filter mapping you're adding should be the first filter listed).

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

Acegi on WebSphere

The 'FilterToBeanProxy' provided by Acegi does not use the ThreadContext ClassLoader, which is required by WebSphere.
replace the Acegi FilterToBeanProxy with the Spring implementation (which uses the Thread ClassLoader) :

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

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

Java codes implements the Acegi classes

Copy those java files to %PROJECT_HOME%/src/java/grails/

grails.GrailsWebApplicationObjectSupport.java - Abstract class that extends WebApplicationObjectSupport to get GrailsApplication.
grails.GrailsDaoImpl.java - Class that implements UserDetailsService to use User Domain Class from Grails Application.
grails.GrailsFilterInvocationDefinition.java - Class that implements FilterInvocationDefinitionSource to use Request Map from Grails Application.

Acegi Bean Configurations

Copy securityContext.xml file to %PROJECT_HOME%/web-app/WEB-INF/

%PROJECT_HOME%/web-app/WEB-INF/securityContext.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 id="filterChainProxy" class="org.acegisecurity.util.FilterChainProxy">
		<property name="filterInvocationDefinitionSource">
			<value>
				CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
				PATTERN_TYPE_APACHE_ANT
				/**=httpSessionContextIntegrationFilter,logoutFilter,
authenticationProcessingFilter,securityContextHolderAwareRequestFilter,rememberMeProcessingFilter,
anonymousProcessingFilter,exceptionTranslationFilter,filterInvocationInterceptor
			</value>
<!-- fix "/**=httpSessionContextIntegrationFilter" to "filterInvocationInterceptor" in one line  -->
		</property>
	</bean>

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

	<bean id="logoutFilter" class="org.acegisecurity.ui.logout.LogoutFilter">
		<!-- After logged out URL-->
		<constructor-arg value="/"/>
		<constructor-arg>
			<list>
				<ref bean="rememberMeServices"/>
				<bean class="org.acegisecurity.ui.logout.SecurityContextLogoutHandler"/>
			</list>
		</constructor-arg>
	</bean>

	<bean id="authenticationProcessingFilter" class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilter">
		<property name="authenticationManager" ref="authenticationManager"/>
		<property name="authenticationFailureUrl" value="/login/authfail?login_error=1"/>
		<property name="defaultTargetUrl" value="/"/>
		<property name="filterProcessesUrl" value="/j_acegi_security_check"/>

		<property name="rememberMeServices" ref="rememberMeServices"/>
	</bean>

	<bean id="securityContextHolderAwareRequestFilter"
		class="org.acegisecurity.wrapper.SecurityContextHolderAwareRequestFilter"/>


	<bean id="anonymousProcessingFilter" class="org.acegisecurity.providers.anonymous.AnonymousProcessingFilter">
		<property name="key" value="foo"/>
		<property name="userAttribute" value="anonymousUser,ROLE_ANONYMOUS"/>
	</bean>

	<bean id="exceptionTranslationFilter" class="org.acegisecurity.ui.ExceptionTranslationFilter">
		<property name="authenticationEntryPoint">
			<bean class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilterEntryPoint">
				<!-- Login form Url -->
				<property name="loginFormUrl" value="/login/auth"/>
				<property name="forceHttps" value="false"/>
			</bean>
		</property>
		<property name="accessDeniedHandler">
			<bean class="org.acegisecurity.ui.AccessDeniedHandlerImpl">
				<!-- Login denied path -->
				<property name="errorPage" value="/login/denied"/>
			</bean>
		</property>
	</bean>

	<bean id="filterInvocationInterceptor" class="org.acegisecurity.intercept.web.FilterSecurityInterceptor">
		<property name="authenticationManager" ref="authenticationManager"/>
		<property name="accessDecisionManager">
			<bean class="org.acegisecurity.vote.AffirmativeBased">
				<property name="allowIfAllAbstainDecisions" value="false"/>
				<property name="decisionVoters">
					<list>
						<bean class="org.acegisecurity.vote.RoleVoter"/>
						<bean class="org.acegisecurity.vote.AuthenticatedVoter"/>
					</list>
				</property>
			</bean>
		</property>
		<property name="objectDefinitionSource" ref="objectDefinitionSource"/>
	</bean>

	<bean id="objectDefinitionSource" class="grails.GrailsFilterInvocationDefinition">
		<property name="loginControllerName" value="login"/>
		<property name="loginControllerRequestMapMethod" value="requestMap"/>

		<property name="requestMapClass" value="Requestmap"/>
		<property name="requestMapPathFieldMethod" value="getUrl"/>
		<property name="requestMapConfigAttributeFieldMethod" value="getConfig_attribute"/>
		<property name="requestMapPathFieldName" value="url"/>

	</bean>


	<bean id="rememberMeProcessingFilter" class="org.acegisecurity.ui.rememberme.RememberMeProcessingFilter">
		<property name="authenticationManager" ref="authenticationManager"/>
		<property name="rememberMeServices" ref="rememberMeServices"/>
	</bean>

	<bean id="rememberMeServices" class="org.acegisecurity.ui.rememberme.TokenBasedRememberMeServices">
		<property name="userDetailsService" ref="userDetailsService"/>
		<property name="key" value="grailsRocks"/>
	</bean>


	<bean id="authenticationManager" class="org.acegisecurity.providers.ProviderManager">
		<property name="providers">
			<list>
				<ref local="daoAuthenticationProvider"/>
				<bean class="org.acegisecurity.providers.anonymous.AnonymousAuthenticationProvider">
					<property name="key" value="foo"/>
				</bean>
				<bean class="org.acegisecurity.providers.rememberme.RememberMeAuthenticationProvider">
					<property name="key" value="grailsRocks"/>
				</bean>
			</list>
		</property>
	</bean>

	<bean id="passwordEncoder" class="org.acegisecurity.providers.encoding.Md5PasswordEncoder"/>

	<bean id="daoAuthenticationProvider" class="org.acegisecurity.providers.dao.DaoAuthenticationProvider">
		<property name="userDetailsService" ref="userDetailsService"/>
		<property name="passwordEncoder"><ref local="passwordEncoder"/></property>
		<property name="userCache">
			<bean class="org.acegisecurity.providers.dao.cache.EhCacheBasedUserCache">
				<property name="cache">
					<bean class="org.springframework.cache.ehcache.EhCacheFactoryBean">
						<property name="cacheManager">
							<bean class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"/>
						</property>
						<property name="cacheName" value="userCache"/>
					</bean>
				</property>
			</bean>
		</property>
	</bean>

	<!-- Grails User Details Service -->
	<bean id="userDetailsService" class="grails.GrailsDaoImpl" >
		<property name="controllerName" value="login"/>
		<property name="controllerLoadUserMethod" value="loadUserByUsername"/>
		<property name="userName" value="getUsername"/>
		<property name="password" value="getPasswd"/>
		<property name="enabled" value="getEnabled"/>
		<property name="controllerLoadAuthoritiesMethod" value="authoritiesByUsername"/>
		<property name="authority" value="getAuthority"/>
	</bean>


	<!-- This bean is optional; it isn't used by any other bean as it only listens and logs -->
	<bean id="loggerListener" class="org.acegisecurity.event.authentication.LoggerListener"/>


</beans>

Sample User Data (ApplicationBootStrap.groovy)

BootStrap.groovy
import org.apache.commons.codec.digest.DigestUtils as DU

class ApplicationBootStrap {

	def init = { servletContext ->
		def pass = DU.md5Hex("pass")
		new Person(username:"admin",passwd:pass,enabled:true).save()
		new Person(username:"user1",passwd:pass,enabled:true).save()
		def supervisor = new Authorities(username:"admin",authority:"ROLE_SUPERVISOR").save()
		def user = new Authorities(username:"user1",authority:"ROLE_USER").save()

		new Requestmap(url:"/book/**",config_attribute:"IS_AUTHENTICATED_REMEMBERED").save()
		new Requestmap(url:"/book/create/**",config_attribute:"ROLE_SUPERVISOR").save()
		new Requestmap(url:"/book/edit/**",config_attribute:"ROLE_USER").save()
		new Requestmap(url:"/authorities/**",config_attribute:"ROLE_SUPERVISOR").save()
		new Requestmap(url:"/person/**",config_attribute:"ROLE_SUPERVISOR").save()
		new Requestmap(url:"/requestmap/**",config_attribute:"ROLE_SUPERVISOR").save()
		new Requestmap(url:"/**",config_attribute:"IS_AUTHENTICATED_ANONYMOUSLY").save()
	}
	def destroy = {
	}
}
Enter labels to add to this page:
Please wait 
Looking for a label? Just start typing.
  1. Oct 27, 2006

    alex shneyderman says:

    Great tutorial and works almost perfectly. A couple of comments: 1. When confi...

    Great tutorial and works almost perfectly.

    A couple of comments:

    1. When configuring request maps, BootStrap.groovy has code like this:new Requestmap(url:"/book/**",config_attribute:"IS_AUTHENTICATED_REMEMBERED").save()

    if controller has mixed letter case in its name something spendRecord the statement above should look like:new Requestmap(url:"/spendrecord/**",config_attribute:"IS_AUTHENTICATED_REMEMBERED").save()
    because Acegi is configured to match lower case uri against what is in requestmap entites' urls

    2. When going thru tutorial and doing cut and paste of snippets make sure that configuration of securityContext.xml that realtes to 
    filterInvocationDefinitionSource the value for /**= setting has to be all on one line. Otherwise you will be getting 500 error. That's all.

  2. Nov 10, 2006

    Jakub Prikazsky says:

    Nice tutorial, I have just one question. I am very little knowledge about Groovy...

    Nice tutorial, I have just one question. I am very little knowledge about Groovy and here is the question: do I have to reload the application when the mapping has changed? I think this was purpose why use Groovy to define the mapping (ApplicatonBootStrap).

    thank you

    James

  3. Nov 10, 2006

    Tsuyoshi Yamamoto says:

    Thanks for all comment. To Alex for mixed letter case Maybe I need to fix Grail...

    Thanks for all comment.

    To Alex
    for mixed letter case
    Maybe I need to fix GrailsFilterInvocationDefinition.java source by adding _url = _url.toLowerCase();

    To Jakub
    You don't have to reload application to change requestmap. and it's just a sample data in BootStrap.
    You can edit Requestmap table to change data. (simply scaffold Requestmap domain class )

  4. Nov 17, 2006

    Bernd Schiffer says:

    In the LoginController you can save code lines: def index = { def auth = SCH.g...

    In the LoginController you can save code lines:

    def index = {
    	def auth = SCH.getContext().getAuthentication();
    	if(auth instanceof AbstractAuthenticationToken){
    		redirect(uri:"/")
    	} else {
    		redirect(action:"auth")
    	}
    	render(text:"login")
    }
    Thanks for the tutorial, Bernd 

  5. Dec 20, 2006

    Tsuyoshi Yamamoto says:

    userCache will not work on 0.4-SNAPSHOT spring-2.0 needs net.sf.ehcache.store....

    userCache will not work on 0.4-SNAPSHOT
    spring-2.0 needs
    net.sf.ehcache.store.MemoryStoreEvictionPolicy of ehcache-1.2 .
    please change following part of the code until

    before
    <bean id="daoAuthenticationProvider" class="org.acegisecurity.providers.dao.DaoAuthenticationProvider">
    	<property name="userDetailsService" ref="userDetailsService"/>
    	<property name="passwordEncoder"><ref local="passwordEncoder"/></property>
    	<property name="userCache">
    		<bean class="org.acegisecurity.providers.dao.cache.EhCacheBasedUserCache">
    			<property name="cache">
    				<bean class="org.springframework.cache.ehcache.EhCacheFactoryBean">
    					<property name="cacheManager">
    						<bean class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"/>
    					</property>
    					<property name="cacheName" value="userCache"/>
    				</bean>
    			</property>
    		</bean>
    	</property>
    </bean>
    After
    <bean id="daoAuthenticationProvider" class="org.acegisecurity.providers.dao.DaoAuthenticationProvider">
    	<property name="userDetailsService" ref="userDetailsService"/>
    	<property name="passwordEncoder"><ref local="passwordEncoder"/></property>
    </bean>
  6. Apr 18, 2007

    Steven Walsh says:

    Hi, I am using grails 0.4.2 and this tutorial doesnt work out of the box. ...

    Hi,

    I am using grails 0.4.2 and this tutorial doesnt work out of the box.  Any chance you could update it?

    Thanks

  7. Apr 19, 2007

    Tsuyoshi Yamamoto says:

    try AcegiSecurity Plugin
  8. Mar 20, 2008

    ejy says:

    This page is completely out of date. The acegi plugin and grails have changed so...

    This page is completely out of date. The acegi plugin and grails have changed so much that this page is really misleading. Please update this accordingly