You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 2 Next »

Why use Acegi?

Acegi is a pretty complete solution for all kinds of security needs. It is very configurable and supports many authentication sources out of the box. These include: database queries, LDAP queries, CAS. It can be a bit complex to set up, but following the how to below should get you started quickly.

The example below shows how you can use Acegi in combination with Wicket-auth-roles. Acegi takes care of the authentication, Wicket-auth-roles does authorization. By this I mean that Acegi looks up the user (including roles, full name, etc.), validates the password, and keeps track of the logged on user in the current session. Wicket-auth-roles validates that the current user (can be none as well) has access to a particular page, or even a particular component.

The advantage of this setup is that you get to use a lot of Acegi functionality out of the box. Besides the mentioned authentication sources, you can for example also protect some services (spring beans) that are called by your Wicket application with an Acegi security proxy. Acegi's security proxies allow role based access (allowed if you have role x) but can also filter the results of a service call (e.g. user a is only allowed to see data where some amount is smaller then x).

Complete example

This example is extracted from a production system running on Wicket 1.2.6, Spring 2.0.5 and Acegi 1.0.2 on a 1.4 JVM. Wicket-auth-roles requires Java 5, but is quite simple to port it to Java 1.4 by removing everything related to annotations.

It is assumed you have a Spring environment. How to set up a Spring environment is not explained here. Either op the options described on XXX will do.

HOWTO TO BE FINISHED!!

Required on the classpath

  • Spring-support (it was assumed you already have this)
  • Acegi
  • Ehcache (optional, but assumed in this example)
  • Wicket-auth-roles

Acegi basic setup

First of all you need to set up Acegi. Somewhere in your Spring configs add:

<!-- Proxy to a set of filters that enforce authentication and authorization. -->
  <bean id="filterChainProxy" class="org.acegisecurity.util.FilterChainProxy">
    <property name="filterInvocationDefinitionSource">
      <value>
        CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
        PATTERN_TYPE_APACHE_ANT
        /**=httpSessionContextIntegrationFilter
      </value>
    </property>
  </bean>

  <!-- Maintains security context between requests (using the session). -->
  <bean id="httpSessionContextIntegrationFilter"
    class="org.acegisecurity.context.HttpSessionContextIntegrationFilter">
    <property name="forceEagerSessionCreation" value="true"/>
  </bean>

   <!-- Users cache for Acegi (Ehcache). -->
   <bean id="userCache" class="org.acegisecurity.providers.dao.cache.EhCacheBasedUserCache">
      <property name="cache">
         <bean class="org.springframework.cache.ehcache.EhCacheFactoryBean">
            <property name="cacheManager" ref="cacheManager"/>
            <property name="cacheName" value="your.application.name.USER_CACHE"/>
         </bean>
      </property>
   </bean>

   <bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"/>

This also configures a user cache (very important when you use LDAP or database authentication). This particular configuration uses an EhCache cache.

Add the following to your web.xml:

    <filter>
        <filter-name>Acegi HTTP Request Security Filter</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 HTTP Request Security Filter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

You now have setup Acegi to put a security token in the 'security context' (which comes down to a thread local variable) during the invocation of each and every request, based on some information in the session. At the end of the request, the security token is removed from the security context.

You will also have to configure an authentication provider. Here is an example that get its data from LDAP.

If Acegi's org.acegisecurity.userdetails.UserDetails does not have enough information, you can extend it with your own. Here we add a full name and an e-mail address.

public interface YourAppUserDetails extends UserDetails {
    String getEmail();
    String getDisplayname();
}

An implementation:

public class YourAppUserDetailsLdapImpl implements YourAppUserDetails, org.acegisecurity.userdetails.ldap.LdapUserDetails {

    private static final String LDAP_ATTRIBUTENAME_MAIL = "mail";
    private static final String LDAP_ATTRIBUTENAME_DISPLAYNAME = "displayname";

    /** The wrapped <code>LdapUserDetails</code> instance. */
    private final LdapUserDetails ldapUserDetails;

    private String email;
    private String displayname;

    /**
     * Wrap the given Acegi <code>LdapUserDetails</code> instance. This class adds the additional properties
     * <code>email</code> and <code>displayname</code> that are fetched from the <code>LdapUserDetails</code>
     * {@link org.acegisecurity.userdetails.ldap.LdapUserDetails#getAttributes() attributes}.
     *
     * Note: an e-mail address is mandatory, full name is not.
     *
     * @param ldapUserDetails the wrapped user details instance
     */
    public YourAppUserDetailsLdapImpl(final LdapUserDetails ldapUserDetails) {
        this.ldapUserDetails = ldapUserDetails;
        Attributes attributes = this.ldapUserDetails.getAttributes();

        // Fetch e-mail address from attributes (required, exception is thrown if not available).
        try {
            Attribute mailAttribute = attributes.get(LDAP_ATTRIBUTENAME_MAIL);
            email = (String) (mailAttribute == null ? null : mailAttribute.get());
            if (email == null) {
                String errorMessage = "No attribute named '" + LDAP_ATTRIBUTENAME_MAIL
                        + "' found for user '" + ldapUserDetails.getUsername() + "'.";
                throw new RuntimeException(errorMessage);
            }

        } catch (NamingException e) {
            String errorMessage = "NamingException while attempting to retrieve value for attribute '"
                    + LDAP_ATTRIBUTENAME_MAIL + "' for user '" + ldapUserDetails.getUsername() + "'.";
            throw new RuntimeException(errorMessage, e);
        }

        // Get Display name
        try {
            Attribute displaynameAttribute = attributes.get(LDAP_ATTRIBUTENAME_DISPLAYNAME);
            displayname = (String) (displaynameAttribute == null ? null : displaynameAttribute.get());

        } catch (NamingException e) {
            LOG.warn("NamingException while attempting to retrieve value for attribute '"
                    + LDAP_ATTRIBUTENAME_DISPLAYNAME
                    + "' for user '" + ldapUserDetails.getUsername() + "'. Setting displayname to null.");
        }
    }

    /** {@inheritDoc} */
    public String getEmail() {
        return email;
    }

    /** {@inheritDoc} */
    public String getDisplayname() {
        return displayname;
    }

    /**
     * @return this user's authorities (i.e. roles)
     */
    public GrantedAuthority[] getAuthorities() {
        return ldapUserDetails.getAuthorities();
    }

    /**
     * @return this user's password
     */
    public String getPassword() {
        return ldapUserDetails.getPassword();
    }

    /**
     * @return this user's user name
     */
    public String getUsername() {
        return ldapUserDetails.getUsername();
    }

    /** {@inheritDoc} */
    public boolean isAccountNonExpired() {
        return ldapUserDetails.isAccountNonExpired();
    }

    /** {@inheritDoc} */
    public boolean isAccountNonLocked() {
        return ldapUserDetails.isAccountNonLocked();
    }

    /** {@inheritDoc} */
    public boolean isCredentialsNonExpired() {
        return ldapUserDetails.isCredentialsNonExpired();
    }

    /** {@inheritDoc} */
    public boolean isEnabled() {
        return ldapUserDetails.isEnabled();
    }

    /**
     * @return this user's LDAP attributes
     */
    public Attributes getAttributes() {
        return ldapUserDetails.getAttributes();
    }

    /**
     * Returns any LDAP response controls that were part of the user authentication process. See
     * <a href="ftp://ftp.isi.edu/in-notes/rfc2251.txt">RFC 2251</a> for a description of controls.
     * @return LDAP response controls
     */
    public Control[] getControls() {
        return ldapUserDetails.getControls();
    }

    /**
     * @return this user's distinguished name
     */
    public String getDn() {
        return ldapUserDetails.getDn();
    }
}

Its a bit messy, improvements are welcome.

We have to write our own LDAP authenticatoin provider as Acegi does not know about the e-mail address and full name:

import org.acegisecurity.providers.ldap.LdapAuthenticationProvider;
import org.acegisecurity.providers.ldap.LdapAuthenticator;
import org.acegisecurity.providers.ldap.LdapAuthoritiesPopulator;

public class YourAppLdapAuthenticationProvider extends LdapAuthenticationProvider {
    public YourAppLdapAuthenticationProvider(LdapAuthenticator authenticator, LdapAuthoritiesPopulator authoritiesPopulator) {
        super(authenticator, authoritiesPopulator);
    }

    /**
     * Creates a <code>UserDetails</code> instance ({@link YourAppUserDetails}) that provides the additional properties
     * <code>email</code> and <code>displayname</code>.
     */
    protected UserDetails createUserDetails(LdapUserDetails ldapUser, String username, String password) {
        UserDetails userDetails = super.createUserDetails(ldapUser, username, password);
        return new YourAppUserDetailsLdapImpl((LdapUserDetails) userDetails);
    }
}

Finally the configuration of the LDAP authenticatoin provider. Again this is done in a Spring config file:

    <!-- Authentication manager, configured with one provider that retrieves authentication information
        from an LDAP instance. -->
    <bean id="authenticationManager" class="org.acegisecurity.providers.ProviderManager">
        <property name="providers">
            <list>
                <ref local="ldapAuthenticationProvider"/>
            </list>
        </property>
    </bean>

    <!-- Example query against Active Directory, uses sAMAccountName as username -->
    <bean id="userSearch" class="org.acegisecurity.ldap.search.FilterBasedLdapUserSearch">
        <constructor-arg index="0" value="ou=users,${__ldap.basedn}" />
        <constructor-arg index="1" value="(&amp;(objectclass=person)(sAMAccountName={0}))" />
        <constructor-arg index="2" ref="initialDirContextFactory" />
        <property name="searchSubtree" value="false" />
    </bean>

    <!-- Authentication provider for authentication via LDAP. -->
    <bean id="ldapAuthenticationProvider" class="com.example.app.security.YourAppLdapAuthenticationProvider">
        <constructor-arg>
            <bean class="org.acegisecurity.providers.ldap.authenticator.BindAuthenticator">
                <constructor-arg ref="initialDirContextFactory"/>
                <property name="userSearch" ref="userSearch" />
            </bean>
        </constructor-arg>
        <constructor-arg>
            <bean class="org.acegisecurity.providers.ldap.populator.DefaultLdapAuthoritiesPopulator">
                <constructor-arg ref="initialDirContextFactory"/>
                <constructor-arg>
                    <value>ou=groups,${__ldap.basedn}</value>
                </constructor-arg>
                <property name="groupSearchFilter" value="member={0}"/>
            </bean>
        </constructor-arg>
    </bean>

    <!-- Initial context factory for JNDI queries to LDAP server. -->
    <bean id="initialDirContextFactory" class="org.acegisecurity.ldap.DefaultInitialDirContextFactory">
        <constructor-arg value="ldap://${__ldap.host}:${__ldap.port}/"/>
        <property name="managerDn" value="${__ldap.manager.cn}"/>
        <property name="managerPassword" value="${__ldap.manager.pass}"/>
    </bean>

    <!-- Read LDAP properties from a file. -->
    <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="placeholderPrefix" value="${__"/>
        <property name="location" value="classpath:ldap.properties"/>
    </bean>

With ldap.properties something like:

ldap.host=192.168.20.123
ldap.port=380
ldap.basedn=ou=Application,ou=Organisation,dc=example,dc=com
ldap.manager.cn=cn=manager,ou=users,ou=Application,ou=Organisation,dc=example,dc=com
ldap.manager.pass=secret

TODO: describe how roles are structured in LDAP tree.

Wicket setup

Your application should extend Wicket-auth-roles's Authenticated AuthenticatedWebApplication. This
example runs on Java 1.4 and therefore requires the MetaDataRoleAuthorizationStrategy. If you are
running on Java 5 or higher, you can also use the annotations based approach. Look at the source
code in Wicket-auth-roles-example for inspiration.

import org.acegisecurity.AuthenticationManager;
import wicket.authentication.AuthenticatedWebApplication;
import wicket.authorization.strategies.role.metadata.MetaDataRoleAuthorizationStrategy;

public class YourAppApplication extends AuthenticatedWebApplication {
    // To be injected by Spring
    private AuthenticationManager authenticationManager;

    protected void init() {
        super.init();

        // ... other settings ...

        // Security settings.
        getSecuritySettings().setAuthorizationStrategy(new MetaDataRoleAuthorizationStrategy(this));

        // List every(!) page here for which access is restricted.
        MetaDataRoleAuthorizationStrategy.authorize(EditPage.class, SecurityConstants.ROLE_GEBRUIKER);
        MetaDataRoleAuthorizationStrategy.authorize(ManagerPage.class, SecurityConstants.ROLE_BEHEERDER);
    }

    protected Class getWebSessionClass() {
        return YourAppSession.class;
    }

    protected Class getSignInPageClass() {
        return YourAppSignIn.class;
    }

    public AuthenticationManager getAuthenticationManager() {
        return authenticationManager;
    }

    // To be injected by Spring
    public void setAuthenticationManager(final AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

}

As you can see we need to set authorization rules for each component (page) we want to restrict
access to. Note that if you are using inheritance, you need to list every sub-class. The application
that this information is extracted from uses a hack to circumvent that. It overrides
wicket.authorization.strategies.role.metadata.InstantiationPermissions to use the authorisation
data of the first higher base-class that has them. See Wicket-XXX for source code.

 
 
 
 
 
  • No labels