Alberto Fernandez
Alberto Fernandez

Reputation: 77

Spring Security 3.1 Active Directory Authentication

I'm connecting to my AD with the next configuration:

    class="org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider">
    <beans:constructor-arg value="mydomain" />
    <beans:constructor-arg value="ldap://my URL :389" />
    <beans:property name="convertSubErrorCodesToExceptions" value="true"/>
</beans:bean>

The connection is working fine, because if I write a wrong login/password, I get "bad credentials" (User was not found in directory)

But if I try with a right login and password, I get an exception:

org.springframework.dao.IncorrectResultSizeDataAccessException: Incorrect result size: expected 1, actual 0
    at org.springframework.security.ldap.SpringSecurityLdapTemplate.searchForSingleEntryInternal(SpringSecurityLdapTemplate.java:239)
    at org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider.searchForUser(ActiveDirectoryLdapAuthenticationProvider.java:258)
....

Upvotes: 0

Views: 12003

Answers (3)

Liqun Chen
Liqun Chen

Reputation: 69

The error IncorrectResultSizeDataAccessException was caused by a bug within org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl

if you look into the following code, when the token seriesId doesn't exist, should not throw the error "more than one value".

public PersistentRememberMeToken getTokenForSeries(String seriesId) {
    try {
        return (PersistentRememberMeToken) tokensBySeriesMapping.findObject(seriesId);
    } catch(IncorrectResultSizeDataAccessException moreThanOne) {
        logger.error("Querying token for series '" + seriesId + "' returned more than one value. Series" +
                " should be unique");
    } catch(DataAccessException e) {
        logger.error("Failed to load token for series " + seriesId, e);
    }

    return null;
}

You can implement you own token repository dao, here is mine:

/**
 * Save/cache the login token, retrieve or update it for remember-me feature.
 * 
 * create table persistent_logins (username varchar(64) not null, series varchar(64) primary key,
 * token varchar(64) not null, last_used timestamp not null)
 * 
 * @author lchen
 * 
 */
public class TokenRepositoryDao extends BaseDao implements PersistentTokenRepository {

    @Override
    public void createNewToken(PersistentRememberMeToken token) {
        String sql = "insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)";
        getJdbcTemplate().update(sql, token.getUsername(), token.getSeries(), token.getTokenValue(), token.getDate());
    }

    @Override
    public PersistentRememberMeToken getTokenForSeries(String series) {
        String sql = "select username,series,token,last_used from persistent_logins where series = ?";
        try {
            return getJdbcTemplate().queryForObject(sql, new PersistentRememberMeTokenMapper(), series);
        } catch (IncorrectResultSizeDataAccessException moreThanOne) {
            if (moreThanOne.getActualSize() > 1)
                logger.error("Querying token for series '" + series + "' returned more than one value. Series" + " should be unique");
        } catch (DataAccessException e) {
            logger.error("Failed to load token for series " + series, e);
        }
        return null;
    }

    @Override
    public void removeUserTokens(String username) {
        String sql = "delete from persistent_logins where username = ?";
        getJdbcTemplate().update(sql, username);
    }

    @Override
    public void updateToken(String series, String tokenValue, Date lastUsed) {
        String sql = "update persistent_logins set token = ?, last_used = ? where series = ?";
        getJdbcTemplate().update(sql, tokenValue, new Date(), series);
    }

    private class PersistentRememberMeTokenMapper implements RowMapper<PersistentRememberMeToken> {
        @Override
        public PersistentRememberMeToken mapRow(ResultSet rs, int rowNum) throws SQLException {
            String username = rs.getString("username");
            String series = rs.getString("series");
            String token = rs.getString("token");
            Date date = rs.getDate("last_used");
            return new PersistentRememberMeToken(username, series, token, date);
        }
    }

}

Following is the workable configs for spring security:

<security:http pattern="/common/**" security="none" />
<security:http pattern="/styles/**" security="none" />
<security:http pattern="/images/**" security="none" />
<security:http pattern="/scripts/**" security="none" />
<security:http pattern="/layouts/**" security="none" />

<security:http use-expressions="true">
    <security:intercept-url pattern="/login.do" access="permitAll" />
    <security:intercept-url pattern="/logout.do" access="permitAll" />
    <security:intercept-url pattern="/login/failure.do" access="permitAll" />
    <security:intercept-url pattern="/index.jsp" access="permitAll" />
    <security:intercept-url pattern="/home/**" access="isAuthenticated()" />
    <security:intercept-url pattern="/upload/**" access="hasRole('ROLE_USER')" />
    <security:intercept-url pattern="/**" access="denyAll" />
    <security:form-login login-page="/login.do" authentication-failure-url="/login/failure.do" default-target-url="/" />
    <security:logout logout-url="/logout.do" logout-success-url="/" delete-cookies="JSESSIONID" />
    <security:remember-me user-service-ref="userDetailsService" token-repository-ref="tokenRepository" token-validity-seconds="1296000" />
</security:http>

<bean id="tokenRepository" class="com.abc.dao.TokenRepositoryDao" />

<security:authentication-manager>
    <security:authentication-provider ref="ldapAuthProvider" />
</security:authentication-manager>

<bean id="userDetailsService" class="org.springframework.security.ldap.userdetails.LdapUserDetailsService">
    <constructor-arg ref="userSearch" />
    <constructor-arg ref="authoritiesPopulator" />
</bean>

<bean id="contextSource" class="org.springframework.security.ldap.DefaultSpringSecurityContextSource">
    <constructor-arg value="ldap://corp.abc.com:389/dc=Corp,dc=abc,dc=com" />
    <property name="userDn" value="***" />
    <property name="password" value="***" />
    <property name="baseEnvironmentProperties">
        <map>
            <entry key="java.naming.referral">
                <value>follow</value> <!-- Avoid error: Unprocessed Continuation Reference(s); remaining name '' -->
            </entry>
        </map>
    </property>
</bean>

<bean id="userSearch" class="org.springframework.security.ldap.search.FilterBasedLdapUserSearch">
    <constructor-arg>
        <value></value> <!-- blank value is required here! -->
    </constructor-arg>
    <constructor-arg>
        <value>(sAMAccountName={0})</value>
    </constructor-arg>
    <constructor-arg ref="contextSource" />
    <property name="searchSubtree">
        <value>true</value>
    </property>
</bean>

<bean id="ldapAuthProvider" class="org.springframework.security.ldap.authentication.LdapAuthenticationProvider">
    <constructor-arg ref="authenticator" />
    <constructor-arg ref="authoritiesPopulator" />
</bean>

<bean id="authenticator" class="org.springframework.security.ldap.authentication.BindAuthenticator">
    <constructor-arg ref="contextSource" />
    <property name="userDnPatterns">
        <list>
            <value>sAMAccountName={0}</value>
        </list>
    </property>
    <property name="userSearch" ref="userSearch" />
</bean>

<bean id="authoritiesPopulator" class="org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator">
    <constructor-arg ref="contextSource" />
    <constructor-arg value="" /> <!-- From the root DN of the context factory -->
    <property name="groupRoleAttribute" value="cn" />
    <property name="rolePrefix" value="ROLE_" />
    <property name="searchSubtree" value="true" />
    <property name="convertToUpperCase" value="true" />
    <property name="ignorePartialResultException">
        <value>false</value>
    </property>
</bean>

Upvotes: 2

Geeb
Geeb

Reputation: 651

I had the same problem IncorrectResultSizeDataAccessException whilst trying to authenticate against Active Directory. I haven't solved this particular issue directly, but I have implemented a workaround, which is fully functional, but does mean you need to have a "service account" username and password to establish communication with AD. I guess it uses the "general" Spring LDAP approach, rather than a special AD one.

I followed the recipe here:

Active Directory Spring Security XML config, on the SpringSource forum

Here's my security-context.xml file, for reference:

<?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"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.1.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.1.xsd
">

<!-- There is some Java based config here, don't forget. -->
<!-- Its not important for this example-->
<context:component-scan base-package="uk.ac.example.ldaptest.security" />

<!-- This is for our Active Dir LDAP implementation -->
<beans:bean id="contextSource"
    class="org.springframework.ldap.core.support.LdapContextSource">
    <beans:property name="url"
        value="LDAP://ads.ntd.example.ac.uk:389" />
    <beans:property name="base" value="dc=ntd,dc=example,dc=ac,dc=uk" />
    <beans:property name="userDn" value="cn=ldap,ou=Service Accounts,ou=Management,ou=example,dc=ntd,dc=example,dc=ac,dc=uk" />
    <beans:property name="password" value="XXXXXXXXX" />
    <beans:property name="pooled" value="true" />
    <!-- AD Specific Setting for avoiding the partial exception error -->
    <beans:property name="referral" value="follow" />
</beans:bean>

<beans:bean id="ldapAuthenticationProvider"
    class="org.springframework.security.ldap.authentication.LdapAuthenticationProvider">
    <beans:constructor-arg>
        <beans:bean
            class="org.springframework.security.ldap.authentication.BindAuthenticator">
            <beans:constructor-arg ref="contextSource" />
            <beans:property name="userSearch">
                <beans:bean id="userSearch"
                    class="org.springframework.security.ldap.search.FilterBasedLdapUserSearch">
                    <beans:constructor-arg index="0" value="" />
                    <beans:constructor-arg index="1" value="(sAMAccountName={0})" />
                    <beans:constructor-arg index="2" ref="contextSource" />
                </beans:bean>
            </beans:property>
        </beans:bean>
    </beans:constructor-arg>
    <beans:constructor-arg>
        <beans:bean
            class="org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator">
            <beans:constructor-arg ref="contextSource" />
            <beans:constructor-arg value="" />
            <beans:property name="groupSearchFilter" value="(member={0})" />
            <beans:property name="searchSubtree" value="true" />
            <!-- Below Settings convert the adds the prefix ROLE_ to roles returned 
                from AD -->
        </beans:bean>
    </beans:constructor-arg>
    <!-- Create the Mapper object that returns our customised User object -->
    <!-- Set up in the Java based config mentioned earlier -->
    <beans:property name="userDetailsContextMapper" ref="myUdcm" />
</beans:bean>

<beans:bean id="authenticationManager"
    class="org.springframework.security.authentication.ProviderManager">
    <beans:constructor-arg>
        <beans:list>
            <beans:ref local="ldapAuthenticationProvider" />
        </beans:list>
    </beans:constructor-arg>
</beans:bean>

<!-- we want all URLs within our application to be secured, requiring the 
    role ROLE_STAFF to access them. LDAP supplies this -->
<http auto-config="true" use-expressions="true"
    authentication-manager-ref="authenticationManager">
    <intercept-url pattern="/resources/**" access="permitAll" />
    <intercept-url pattern="/**" access="hasRole('ROLE_STAFF')" />
    <session-management>
        <concurrency-control max-sessions="1" />
    </session-management>
</http>

Upvotes: 1

Tšeliso Molukanele
Tšeliso Molukanele

Reputation: 1660

Check that the search filter used is consistent with your active directory records.

I had the same exception in my web app recently. The user credential were correct and the ActiveDirectoryLdapAuthenticationProvider was binding/authenticating correctly. The failure occurred after binding when searching for groups and other attributes for the authenticated record.

If you look at the code in ActiveDirectoryLdapAuthenticationProvider it has hard coded values for the search filter and it always uses the bind principal to search.

this method

private DirContextOperations searchForUser(DirContext ctx, String username) throws NamingException {
    SearchControls searchCtls = new SearchControls();
    searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE);

    String searchFilter = "(&(objectClass=user)(userPrincipalName={0}))";

    final String bindPrincipal = createBindPrincipal(username);

    String searchRoot = rootDn != null ? rootDn : searchRootFromPrincipal(bindPrincipal);

    return SpringSecurityLdapTemplate.searchForSingleEntryInternal(ctx, searchCtls, searchRoot, searchFilter,
            new Object[]{bindPrincipal});
}

A Jira issue has been submitted and already has a patch.

Upvotes: 4

Related Questions